From c928b4fa4423a1a0d6f5b4461d691d32a5e8c86b Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 21 Apr 2025 20:48:28 +0800 Subject: [PATCH 001/228] feat(chatbot): add chatbot --- packages/components/_util/reactify.ts | 138 ++++++++ .../components/chatbot/_example/basic.tsx | 327 ++++++++++++++++++ packages/components/chatbot/chatbot.en-US.md | 26 ++ packages/components/chatbot/chatbot.md | 36 ++ packages/components/chatbot/index.ts | 11 + packages/components/index.ts | 3 +- packages/tdesign-react/site/site.config.mjs | 15 + 7 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 packages/components/_util/reactify.ts create mode 100644 packages/components/chatbot/_example/basic.tsx create mode 100644 packages/components/chatbot/chatbot.en-US.md create mode 100644 packages/components/chatbot/chatbot.md create mode 100644 packages/components/chatbot/index.ts diff --git a/packages/components/_util/reactify.ts b/packages/components/_util/reactify.ts new file mode 100644 index 0000000000..6265c5019f --- /dev/null +++ b/packages/components/_util/reactify.ts @@ -0,0 +1,138 @@ +import React, { Component, createRef, createElement, forwardRef } from 'react'; + +type AnyProps = { + [key: string]: any; +}; + +const hyphenateRE = /\B([A-Z])/g; + +export function hyphenate(str: string): string { + return str.replace(hyphenateRE, '-$1').toLowerCase(); +} + +const styleObjectToString = (style: CSSRule) => { + const unitlessKeys = new Set([ + 'animationIterationCount', + 'boxFlex', + 'boxFlexGroup', + 'boxOrdinalGroup', + 'columnCount', + 'fillOpacity', + 'flex', + 'flexGrow', + 'flexShrink', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', + ]); + + return Object.entries(style) + .filter(([_, value]) => value != null && value !== '') // 过滤无效值 + .map(([key, value]) => { + // 转换驼峰式为连字符格式 + const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); + + // 处理数值类型值 + let cssValue = value; + if (typeof value === 'number' && value !== 0 && !unitlessKeys.has(key)) { + cssValue = `${value}px`; + } + + return `${cssKey}:${cssValue};`; + }) + .join(' '); +}; + +const reactify = ( + WC: string, +): React.ForwardRefExoticComponent & React.RefAttributes> => { + class Reactify extends Component { + eventHandlers: [string, EventListener][]; + + ref: React.RefObject; + + constructor(props: any) { + super(props); + this.eventHandlers = []; + const { innerRef } = props; + this.ref = innerRef || createRef(); + } + + setEvent(event: string, val: EventListener) { + this.eventHandlers.push([event, val]); + this.ref.current?.addEventListener(event, val); + } + + update() { + this.clearEventHandlers(); + if (!this.ref.current) return; + Object.entries(this.props).forEach(([prop, val]) => { + if (['innerRef', 'children'].includes(prop)) return; + // event handler + if (typeof val === 'function') { + if (prop.match(/^on[A-Za-z]/)) { + const eventName = prop.slice(2); + const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); + return this.setEvent(omiEventName, val); + } + } + // Complex object + if (typeof val === 'object') { + console.log('====prop', prop); + if (prop === 'style') { + this.ref.current?.setAttribute('style', styleObjectToString(val)); + } else { + (this.ref.current as any)[prop] = val; + } + return; + } + // camel case + if (prop.match(hyphenateRE)) { + this.ref.current?.setAttribute(hyphenate(prop), val); + this.ref.current?.removeAttribute(prop); + return; + } + + return true; + }); + } + + componentDidUpdate() { + this.update(); + } + + componentDidMount() { + this.update(); + } + + componentWillUnmount() { + this.clearEventHandlers(); + } + + clearEventHandlers() { + this.eventHandlers.forEach(([event, handler]) => { + this.ref.current?.removeEventListener(event, handler); + }); + this.eventHandlers = []; + } + + render() { + const { children, className, innerRef, ...rest } = this.props; + + return createElement(WC, { class: className, ...rest, ref: this.ref }, children); + } + } + + return forwardRef((props, ref) => { + return createElement(Reactify, { ...props, innerRef: ref }); + }) as React.ForwardRefExoticComponent & React.RefAttributes>; +}; + +export default reactify; diff --git a/packages/components/chatbot/_example/basic.tsx b/packages/components/chatbot/_example/basic.tsx new file mode 100644 index 0000000000..7eff93d65d --- /dev/null +++ b/packages/components/chatbot/_example/basic.tsx @@ -0,0 +1,327 @@ +import React, { useRef } from 'react'; +import type { + SSEChunkData, + TdChatMessageConfig, + AIMessageContent, + RequestParams, + ChatMessage, + ChatServiceConfig, + TdChatCustomRenderConfig, +} from 'tdesign-react'; +import { ChatBot } from 'tdesign-react'; + +// 扩展自定义消息体类型 +declare module '@tencent/tdesign-chatbot-dev/lib/chatbot/core/type.d.ts' { + interface AIContentTypeOverrides { + weather: BaseContent< + 'weather', + { + temp: number; + city: string; + conditions: string; + } + >; + } +} + +// 默认初始化消息 +const mockData: ChatMessage[] = [ + { + id: '123', + role: 'user', + status: 'complete', + content: [ + { + type: 'text', + data: '南极的自动提款机叫什么名字?', + }, + ], + }, + { + id: '223', + role: 'assistant', + status: 'complete', + content: [ + { + type: 'search', + status: 'complete', + data: { + title: '共找到10个相关内容', + references: [ + { + title: '10本高口碑悬疑推理小说,情节高能刺激,看得让人汗毛直立!', + url: 'http://mp.weixin.qq.com/s?src=11×tamp=1742897036&ver=5890&signature=USoIrxrKY*KWNmBLZTGo-**yUaxdhqowiMPr0wsVhH*dOUB3GUjYcBVG86Dyg7-TkQVrr0efPvrqSa1GJFjUQgQMtZFX5wxjbf8TcWkoUxOrTA7NsjfNQQoVY5CckmJj&new=1', + type: 'mp', + }, + { + title: '悬疑小说下载:免费畅读最新悬疑大作!', + url: 'http://mp.weixin.qq.com/s?src=11×tamp=1742897036&ver=5890&signature=UCc6xbIGsYEyfytL2IC6b3vXlaBcbEJCi98ZVK38vdoFEEulJ3J-95bNkE8Fiv5-pJ5iH75DfJAz6kGX2TSscSisBNW1u6nCPbP-Ue4HxCAfjU8DpUwaOXkFz3*T71rU&new=1', + type: 'mp', + }, + ], + }, + }, + { + type: 'thinking', + status: 'complete', + data: { + title: '思考完成(耗时3s)', + text: 'mock分析语境,首先,Omi是一个基于Web Components的前端框架,和Vue的用法可能不太一样。Vue里的v-html指令用于将字符串作为HTML渲染,防止XSS攻击的话需要信任内容。Omi有没有类似的功能呢?mock分析语境,首先,Omi是一个基于Web Components的前端框架,和Vue的用法可能不太一样。Vue里的v-html指令用于将字符串作为HTML渲染,防止XSS攻击的话需要信任内容。Omi有没有类似的功能呢?', + }, + }, + // { + // type: 'weather', + // id: 'w1', + // data: { + // temp: 1, + // city: '北京', + // conditions: '多云', + // }, + // }, + { + type: 'text', + data: '它叫 [McMurdo Station ATM](#promptId=atm),是美国富国银行安装在南极洲最大科学中心麦克默多站的一台自动提款机。', + }, + { + type: 'suggestion', + status: 'complete', + data: [ + { + title: '《六姊妹》中有哪些观众喜欢的剧情点?', + prompt: '《六姊妹》中有哪些观众喜欢的剧情点?', + }, + { + title: '两部剧在演员表现上有什么不同?', + prompt: '两部剧在演员表现上有什么不同?', + }, + { + title: '《六姊妹》有哪些负面的评价?', + prompt: '《六姊妹》有哪些负面的评价?', + }, + ], + }, + ], + }, + { + id: '789', + role: 'user', + status: 'complete', + content: [ + { + type: 'text', + data: '分析下以下内容,总结一篇广告策划方案', + }, + { + type: 'attachment', + data: [ + { + fileType: 'doc', + name: 'demo.docx', + url: 'https://tdesign.gtimg.com/site/demo.docx', + size: 12312, + }, + { + fileType: 'pdf', + name: 'demo2.pdf', + url: 'https://tdesign.gtimg.com/site/demo.pdf', + size: 34333, + }, + ], + }, + ], + }, + { + id: '34234', + status: 'error', + role: 'assistant', + content: [ + { + type: 'text', + data: '出错了', + }, + ], + }, + { + id: '7389', + role: 'user', + status: 'complete', + content: [ + { + type: 'text', + data: '这张图里的帅哥是谁', + }, + { + type: 'attachment', + data: [ + { + fileType: 'image', + name: 'avatar.jpg', + size: 234234, + url: 'https://tdesign.gtimg.com/site/avatar.jpg', + }, + ], + }, + ], + }, + { + id: '3242', + role: 'assistant', + status: 'complete', + comment: 'good', + content: [ + { + type: 'markdown', + data: '**tdesign** 团队的 *核心开发者* `uyarnchen` 是也。', + }, + ], + }, +]; + +// 自定义渲染-注册插槽规则 +const customRenderConfig: TdChatCustomRenderConfig = { + weather: (content) => ({ + slotName: `${content.type}-${content.id}`, + }), +}; + +const ChatBotReact: React.FC = () => { + const chatRef = useRef(null); + const [mockMessage, setMockMessage] = React.useState(mockData); + + // 消息属性配置 + const messageProps: TdChatMessageConfig = { + user: { + variant: 'base', + placement: 'right', + customRenderConfig, + }, + assistant: { + placement: 'left', + onActions: { + good: async ({ message, active }) => { + // 点赞 + }, + bad: async ({ message, active }) => { + // 点踩 + }, + // suggestion: ({ prompt }) => { + // // 点建议问题 + // chatRef?.current?.addPrompt(prompt); + // }, + }, + chatContentProps: { + search: { + expandable: true, + }, + }, + customRenderConfig, + }, + }; + + // 聊天服务配置 + const chatServiceConfig: ChatServiceConfig = { + // 对话服务地址 + endpoint: 'http://localhost:3000/sse/normal', + stream: true, + // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) + onComplete: (aborted: boolean, params: RequestInit) => { + console.log('onComplete', aborted, params); + }, + // 流式对话过程中出错业务自定义行为 + onError: (err: Error | Response) => { + console.error('Chatservice Error:', err); + }, + // 流式对话过程中用户主动结束对话业务自定义行为 + onAbort: async () => {}, + onMessage: (chunk: SSEChunkData): AIMessageContent => { + const { type, ...rest } = chunk.data; + switch (type) { + case 'search': + // 搜索 + return { + type: 'search', + data: { + title: rest.title || `搜索到${rest?.docs.length}条内容`, + references: rest?.docs, + }, + }; + // 思考 + case 'think': + return { + type: 'thinking', + data: { + title: rest.title || '深度思考中', + text: rest.content || '', + }, + }; + // 正文 + case 'text': + return { + type: 'markdown', + data: rest?.msg || '', + }; + // 自定义-天气 + case 'weather': + return { + ...chunk.data, + data: { ...JSON.parse(chunk.data.content) }, + }; + // 报错 + case 'error': + return { + type: 'text', + status: 'error', + data: rest?.content || '系统繁忙', + }; + default: + return { + type: 'text', + data: chunk?.event === 'complete' ? '' : JSON.stringify(chunk.data), + }; + } + }, + // 自定义请求参数 + onRequest: (innerParams: RequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'Content-Type': 'text/event-stream', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'ewrwerwer', + prompt, + }), + }; + }, + }; + + // useEffect(() => { + // if (!chatRef?.current) { + // return; + // } + // const chat = chatRef.current; + // const update = (e: CustomEvent) => { + // setMockMessage(e.detail); + // } + // chat.addEventListener('message_change', update); + // return () => { + // chat.removeEventListener('message_change', update) + // } + // }, []); + + return ( + + {/* 🌟 自定义输入框左侧区域slot,可以增加模型选项 */} +
+ + ); +}; + +export default ChatBotReact; diff --git a/packages/components/chatbot/chatbot.en-US.md b/packages/components/chatbot/chatbot.en-US.md new file mode 100644 index 0000000000..c417d90bbb --- /dev/null +++ b/packages/components/chatbot/chatbot.en-US.md @@ -0,0 +1,26 @@ +:: BASE_DOC :: + +## API +### Button Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | N +block | Boolean | false | make button to be a block-level element | N +children | TNode | - | button's children elements。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +content | TNode | - | button's children elements。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +disabled | Boolean | false | disable the button, make it can not be clicked | N +form | String | undefined | native `form` attribute,which supports triggering events for a form with a specified id through the use of the form attribute. | N +ghost | Boolean | false | make background-color to be transparent | N +href | String | - | \- | N +icon | TElement | - | use it to set left icon in button。Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +loading | Boolean | false | set button to be loading state | N +shape | String | rectangle | button shape。options:rectangle/square/round/circle | N +size | String | medium | a button has three size。options:small/medium/large。Typescript:`SizeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +suffix | TElement | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +tag | String | - | HTML Tag Element。options:button/a/div | N +theme | String | - | button theme。options:default/primary/danger/warning/success | N +type | String | button | type of button element in html。options:submit/reset/button | N +variant | String | base | variant of button。options:base/outline/dashed/text | N +onClick | Function | | Typescript:`(e: MouseEvent) => void`
trigger on click | N diff --git a/packages/components/chatbot/chatbot.md b/packages/components/chatbot/chatbot.md new file mode 100644 index 0000000000..2daf408cd7 --- /dev/null +++ b/packages/components/chatbot/chatbot.md @@ -0,0 +1,36 @@ +--- +title: Chatbot 智能对话 +description: 智能对话 +isComponent: true +usage: { title: '', description: '' } +spline: navigation +--- + +### 基础用法 + +{{ basic }} + +## API +### Chatbot Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +block | Boolean | false | 是否为块级元素 | N +children | TNode | - | 按钮内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +content | TNode | - | 按钮内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +disabled | Boolean | false | 禁用状态 | N +form | String | undefined | 原生的form属性,支持用于通过 form 属性触发对应 id 的 form 的表单事件 | N +ghost | Boolean | false | 是否为幽灵按钮(镂空按钮) | N +href | String | - | 跳转地址。href 存在时,按钮标签默认使用 `` 渲染;如果指定了 `tag` 则使用指定的标签渲染 | N +icon | TElement | - | 按钮内部图标,可完全自定义。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +loading | Boolean | false | 是否显示为加载状态 | N +shape | String | rectangle | 按钮形状,有 4 种:长方形、正方形、圆角长方形、圆形。可选项:rectangle/square/round/circle | N +size | String | medium | 组件尺寸。可选项:small/medium/large。TS 类型:`SizeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +suffix | TElement | - | 右侧内容,可用于定义右侧图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +tag | String | - | 渲染按钮的 HTML 标签,默认使用标签 ` + + +
+ + ); +}; + +export default ChatSenderExample; diff --git a/packages/components/chat-sender/chat-sender.md b/packages/components/chat-sender/chat-sender.md index 8d95c682e6..49d393bbd7 100644 --- a/packages/components/chat-sender/chat-sender.md +++ b/packages/components/chat-sender/chat-sender.md @@ -1,6 +1,6 @@ --- title: ChatSender 对话输入 -description: 对话输入 +description: 用于构建智能对话场景下的输入框组件 isComponent: true usage: { title: '', description: '' } spline: navigation @@ -10,6 +10,14 @@ spline: navigation {{ base }} +## 附件输入 + +{{ attachment }} + + +## 自定义 + +{{ custom }} ## API ### Chatbot Props diff --git a/packages/components/chat-sender/index.ts b/packages/components/chat-sender/index.ts index feec9a30cf..22c77f2536 100644 --- a/packages/components/chat-sender/index.ts +++ b/packages/components/chat-sender/index.ts @@ -2,7 +2,7 @@ import { TdChatSenderProps } from '@tencent/tdesign-chatbot'; import reactify from '../_util/reactify'; export const ChatSender: React.ForwardRefExoticComponent< - Omit & React.RefAttributes + Omit & React.RefAttributes > = reactify('t-chat-sender'); export default ChatSender; From 9cbb4f0bca8d7cf50124144eb3e1de8657912288 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Sun, 27 Apr 2025 21:12:18 +0800 Subject: [PATCH 021/228] feat(chatsender): chatsender custom api --- package.json | 2 +- .../chat-sender/_example/attachment.tsx | 3 +- .../chat-sender/_example/custom.tsx | 32 +++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index cd30457cc0..ed137dcbc8 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,6 @@ "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0", - "@tencent/tdesign-chatbot": "1.0.0-beta.42" + "@tencent/tdesign-chatbot": "1.0.0-beta.43" } } diff --git a/packages/components/chat-sender/_example/attachment.tsx b/packages/components/chat-sender/_example/attachment.tsx index d228575cc4..b082b32a70 100644 --- a/packages/components/chat-sender/_example/attachment.tsx +++ b/packages/components/chat-sender/_example/attachment.tsx @@ -4,7 +4,6 @@ import { ChatSender, TdAttachmentItem, UploadFile } from 'tdesign-react'; const ChatSenderExample = () => { const [inputValue, setInputValue] = useState('输入内容'); const [loading, setLoading] = useState(false); - const [uploadFile, setUploadFile] = useState(null); const [files, setFiles] = useState([ { name: 'excel-file.xlsx', @@ -39,7 +38,7 @@ const ChatSenderExample = () => { setLoading(false); }; - const onAttachmentsRemove = (e: CustomEvent) => { + const onAttachmentsRemove = (e: CustomEvent) => { console.log('onAttachmentsRemove', e); setFiles(e.detail); }; diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index eccd91a5a0..a77cf5b580 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -1,10 +1,11 @@ -import React, { useState } from 'react'; -import { EnterIcon, InternetIcon } from 'tdesign-icons-react'; -import { ChatSender, Space, Button } from 'tdesign-react'; +import React, { useRef, useState } from 'react'; +import { EnterIcon, InternetIcon, AttachIcon } from 'tdesign-icons-react'; +import { ChatSender, Space, Button, Tag } from 'tdesign-react'; const ChatSenderExample = () => { const [inputValue, setInputValue] = useState('输入内容'); const [loading, setLoading] = useState(false); + const senderRef = useRef(null); // 输入变化处理 const handleChange = (e) => { @@ -25,15 +26,26 @@ const ChatSenderExample = () => { setLoading(false); }; + const onAttachClick = () => { + // senderRef.current?.focus(); + senderRef.current?.selectFile(); + }; + + const onFileSelect = (e: CustomEvent) => { + console.log('===selectfile', e.detail); + }; + return ( {/* 自定义输入框上方区域,可用来引用内容或提示场景 */}
@@ -53,16 +65,24 @@ const ChatSenderExample = () => {
+ {/* 自定义输入框底部区域slot,可以增加模型选项 */}
- -
+ {/* 自定义输入框左侧区域slot,实现触发附件上传 */} +
+ + AI编程 + +
); }; From a96a8773d8239f824efdc8dd4a3a23e44d0bde65 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Sun, 27 Apr 2025 21:39:58 +0800 Subject: [PATCH 022/228] feat(chatsencer): custom demo --- .../chat-sender/_example/custom.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index a77cf5b580..bc1e470166 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -1,9 +1,18 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { EnterIcon, InternetIcon, AttachIcon } from 'tdesign-icons-react'; import { ChatSender, Space, Button, Tag } from 'tdesign-react'; +const classStyles = ` + +`; + const ChatSenderExample = () => { - const [inputValue, setInputValue] = useState('输入内容'); + const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); const senderRef = useRef(null); @@ -35,11 +44,22 @@ const ChatSenderExample = () => { console.log('===selectfile', e.detail); }; + useEffect(() => { + // 创建样式元素并添加 + const styleElement = document.createElement('style'); + styleElement.innerHTML = classStyles; + document.head.appendChild(styleElement); + + return () => { + document.head.removeChild(styleElement); + }; + }, []); + return ( { {/* 自定义输入框左侧区域slot,实现触发附件上传 */}
- AI编程 + 帮我写作
From 630a54882477fe733999b0e0e33efd54d6df15f8 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 28 Apr 2025 11:07:13 +0800 Subject: [PATCH 023/228] feat(chatsender): add var style --- .../chat-sender/_example/custom.tsx | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index bc1e470166..dc02f4085e 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -2,19 +2,37 @@ import React, { useRef, useState, useEffect } from 'react'; import { EnterIcon, InternetIcon, AttachIcon } from 'tdesign-icons-react'; import { ChatSender, Space, Button, Tag } from 'tdesign-react'; -const classStyles = ` - -`; - const ChatSenderExample = () => { const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); const senderRef = useRef(null); + const styleId = useRef(`chat-sender-styles-${Math.random().toString(36).substr(2, 9)}`); + + // 使用变量生成自定义组件样式 + const generateScopedStyles = () => ` + .${styleId.current} { + --td-text-color-placeholder: #DFE2E7; + --td-bg-color-secondarycontainer: #fff; + } + `; + + useEffect(() => { + const styleElement = document.createElement('style'); + styleElement.innerHTML = generateScopedStyles(); + document.head.appendChild(styleElement); + + // 为容器添加唯一类名 + if (senderRef.current) { + senderRef.current.classList.add(styleId.current); + } + + return () => { + document.head.removeChild(styleElement); + if (senderRef.current) { + senderRef.current.classList.remove(styleId.current); + } + }; + }, []); // 输入变化处理 const handleChange = (e) => { @@ -44,17 +62,6 @@ const ChatSenderExample = () => { console.log('===selectfile', e.detail); }; - useEffect(() => { - // 创建样式元素并添加 - const styleElement = document.createElement('style'); - styleElement.innerHTML = classStyles; - document.head.appendChild(styleElement); - - return () => { - document.head.removeChild(styleElement); - }; - }, []); - return ( Date: Mon, 28 Apr 2025 11:55:00 +0800 Subject: [PATCH 024/228] feat(chatsender): custum demo --- .../chat-sender/_example/custom.tsx | 105 +++++++++++++----- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index dc02f4085e..6683d47e79 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -1,15 +1,40 @@ import React, { useRef, useState, useEffect } from 'react'; -import { EnterIcon, InternetIcon, AttachIcon } from 'tdesign-icons-react'; -import { ChatSender, Space, Button, Tag } from 'tdesign-react'; +import { EnterIcon, InternetIcon, AttachIcon, CloseIcon } from 'tdesign-icons-react'; +import { ChatSender, Space, Button, Tag, Dropdown } from 'tdesign-react'; + +const options = [ + { + content: '帮我写作', + value: 1, + placeholder: '输入你要撰写的主题', + }, + { + content: '图像生成', + value: 2, + placeholder: '说说你的创作灵感', + }, + { + content: '网页摘要', + value: 3, + placeholder: '输入你要解读的网页地址', + }, +]; const ChatSenderExample = () => { const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); const senderRef = useRef(null); + const [scene, setScene] = useState(1); + const [showRef, setShowRef] = useState(true); + const [activeR1, setR1Active] = useState(true); + const [activeSearch, setSearchActive] = useState(true); const styleId = useRef(`chat-sender-styles-${Math.random().toString(36).substr(2, 9)}`); // 使用变量生成自定义组件样式 const generateScopedStyles = () => ` + .t-popup__content { + padding: 0; + } .${styleId.current} { --td-text-color-placeholder: #DFE2E7; --td-bg-color-secondarycontainer: #fff; @@ -62,11 +87,20 @@ const ChatSenderExample = () => { console.log('===selectfile', e.detail); }; + const switchScene = (data) => { + console.log('switchScene', data.value); + setScene(data.value); + }; + + const onRemoveRef = () => { + setShowRef(false); + }; + return ( item.value === scene)[0].placeholder} loading={loading} autosize={{ minRows: 2 }} onChange={handleChange} @@ -75,40 +109,61 @@ const ChatSenderExample = () => { onFileSelect={onFileSelect} > {/* 自定义输入框上方区域,可用来引用内容或提示场景 */} -
- - - - 引用一段文字 + {showRef && ( +
+ + + + 引用一段文字 + +
+ +
- -
+
+ )} {/* 自定义输入框底部区域slot,可以增加模型选项 */}
-
- {/* 自定义输入框左侧区域slot,实现触发附件上传 */} + {/* 自定义输入框左侧区域slot,可以用来触发工具场景切换 */}
- - 帮我写作 - + + + {options.filter((item) => item.value === scene)[0].content} + +
); From 86f19a79dfce829e447df3f8a2f8bde48811c054 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 28 Apr 2025 15:40:42 +0800 Subject: [PATCH 025/228] feat(chatbot): chatsender custom action --- package.json | 2 +- packages/components/chat-sender/_example/custom.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ed137dcbc8..722cb98bfb 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,6 @@ "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0", - "@tencent/tdesign-chatbot": "1.0.0-beta.43" + "@r2wc/react-to-web-component": "^1.0.0" } } diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index 6683d47e79..bacf3e9743 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState, useEffect } from 'react'; -import { EnterIcon, InternetIcon, AttachIcon, CloseIcon } from 'tdesign-icons-react'; +import { EnterIcon, InternetIcon, AttachIcon, CloseIcon, ArrowUpIcon, StopIcon } from 'tdesign-icons-react'; import { ChatSender, Space, Button, Tag, Dropdown } from 'tdesign-react'; const options = [ @@ -50,7 +50,6 @@ const ChatSenderExample = () => { if (senderRef.current) { senderRef.current.classList.add(styleId.current); } - return () => { document.head.removeChild(styleElement); if (senderRef.current) { @@ -88,7 +87,6 @@ const ChatSenderExample = () => { }; const switchScene = (data) => { - console.log('switchScene', data.value); setScene(data.value); }; @@ -165,6 +163,14 @@ const ChatSenderExample = () => { + {/* 自定义提交区域slot */} +
+ {!loading ? ( + + ) : ( + + )} +
); }; From c47d13c31bf7ca16842e8098b49a51e1f5de388f Mon Sep 17 00:00:00 2001 From: carolin913 Date: Tue, 29 Apr 2025 18:26:48 +0800 Subject: [PATCH 026/228] feat(chatmessage): custom content --- package.json | 2 +- .../components/chat-message/_example/base.tsx | 45 ++------ .../chat-message/_example/configure.tsx | 71 ++++++++++++ .../chat-message/_example/content.tsx | 107 +++++++++++++++++ .../chat-message/_example/custom.tsx | 109 ++++++++++++++++++ .../chat-message/_example/status.tsx | 49 ++++++++ .../components/chat-message/chat-message.md | 18 ++- packages/components/chat-message/index.ts | 2 +- .../chat-sender/_example/custom.tsx | 8 +- .../components/chat-sender/_example/style.css | 4 + packages/tdesign-react/package.json | 3 +- 11 files changed, 380 insertions(+), 38 deletions(-) create mode 100644 packages/components/chat-message/_example/configure.tsx create mode 100644 packages/components/chat-message/_example/content.tsx create mode 100644 packages/components/chat-message/_example/custom.tsx create mode 100644 packages/components/chat-message/_example/status.tsx create mode 100644 packages/components/chat-sender/_example/style.css diff --git a/package.json b/package.json index 722cb98bfb..6246ef8b57 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,6 @@ "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0", - "@r2wc/react-to-web-component": "^1.0.0" + "@tencent/tdesign-chatbot": "1.0.0-beta.45" } } diff --git a/packages/components/chat-message/_example/base.tsx b/packages/components/chat-message/_example/base.tsx index 9893155a06..84652f6d00 100644 --- a/packages/components/chat-message/_example/base.tsx +++ b/packages/components/chat-message/_example/base.tsx @@ -1,42 +1,21 @@ import React from 'react'; import { ChatMessage, Space } from 'tdesign-react'; +const message = { + content: [ + { + type: 'text', + data: '牛顿第一定律是否适用于所有参考系?', + }, + ], +}; + export default function ChatMessageExample() { return ( - - + + + ); } diff --git a/packages/components/chat-message/_example/configure.tsx b/packages/components/chat-message/_example/configure.tsx new file mode 100644 index 0000000000..c4515581b6 --- /dev/null +++ b/packages/components/chat-message/_example/configure.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { AIMessage, ChatMessage, Divider, Space, SystemMessage, UserMessage } from 'tdesign-react'; + +const messages = { + ai: { + id: '11111', + role: 'assistant', + content: [ + { + type: 'text', + data: '牛顿第一定律并不适用于所有参考系,它只适用于惯性参考系。在质点不受外力作用时,能够判断出质点静止或作匀速直线运动的参考系一定是惯性参考系,因此只有在惯性参考系中牛顿第一定律才适用。', + }, + ], + } as AIMessage, + user: { + id: '22222', + role: 'user', + content: [ + { + type: 'text', + data: '牛顿第一定律是否适用于所有参考系?', + }, + ], + } as UserMessage, + system: { + id: '33333', + role: 'system', + content: [ + { + type: 'text', + data: '模型由 hunyuan 变为 GPT4', + }, + ], + } as SystemMessage, + error: { + id: '4444', + role: 'assistant', + status: 'error', + content: [ + { + type: 'text', + data: '数据解析失败', + }, + ], + } as AIMessage, +}; + +export default function ChatMessageExample() { + return ( + + 可配置角色,头像,昵称,时间 + + + 可配置位置 + + + 角色为system的系统消息 + + + ); +} diff --git a/packages/components/chat-message/_example/content.tsx b/packages/components/chat-message/_example/content.tsx new file mode 100644 index 0000000000..1f60ea27b3 --- /dev/null +++ b/packages/components/chat-message/_example/content.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { AIMessage, ChatMessage, Divider, Space, SystemMessage, UserMessage } from 'tdesign-react'; + +const messages = { + thinking: { + id: '11111', + role: 'assistant', + status: 'streaming', + content: [ + { + type: 'thinking', + data: { + title: '深度思考中...', + text: '好的,我现在需要回答用户关于近三年当代偶像爱情剧创作中需要规避的因素的问题。首先,我需要确定用户的问题类型,使用answer_framework_search查询对应的回答框架', + }, + }, + ], + } as AIMessage, + search: { + id: '22222', + role: 'assistant', + content: [ + { + type: 'search', + data: { + title: '搜索到10篇相关内容', + references: [ + { + title: '10本高口碑悬疑推理小说,情节高能刺激,看得让人汗毛直立!', + url: '', + }, + { + title: '悬疑小说下载:免费畅读最新悬疑大作!', + url: '', + }, + ], + }, + }, + ], + } as AIMessage, + suggestion: { + id: '33333', + role: 'assistant', + content: [ + { + type: 'suggestion', + data: [ + { + title: '《六姊妹》中有哪些观众喜欢的剧情点?', + prompt: '《六姊妹》中有哪些观众喜欢的剧情点?', + }, + { + title: '两部剧在演员表现上有什么不同?', + prompt: '两部剧在演员表现上有什么不同?', + }, + { + title: '《六姊妹》有哪些负面的评价?', + prompt: '《六姊妹》有哪些负面的评价?', + }, + ], + }, + ], + } as AIMessage, + markdown: { + id: '4444', + role: 'assistant', + content: [ + { + type: 'markdown', + data: '**牛顿第一定律** 并不适用于所有参考系,它只适用于 `惯性参考系`', + }, + ], + } as AIMessage, +}; + +export default function ChatMessageExample() { + const onActions = { + suggestion: ({ content }) => { + console.log('suggestionItem', content); + }, + searchItem: ({ content, event }) => { + event.preventDefault(); + event.stopPropagation(); + console.log('searchItem', content); + }, + }; + return ( + + 渲染思考内容 + + 渲染搜索内容 + + 渲染建议问题 + + 渲染Markdown内容 + + + ); +} diff --git a/packages/components/chat-message/_example/custom.tsx b/packages/components/chat-message/_example/custom.tsx new file mode 100644 index 0000000000..17fe6ab6bb --- /dev/null +++ b/packages/components/chat-message/_example/custom.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import TvisionTcharts from 'tvision-charts-react'; +import { AIMessage, BaseContent, ChatMessage, Space } from 'tdesign-react'; + +// 扩展自定义消息体类型 +declare module 'tdesign-react' { + interface AIContentTypeOverrides { + chart: BaseContent< + 'chart', + { + chartType: string; + options: any; + theme: string; + } + >; + } +} + +const message: any = { + id: '123123', + role: 'assistant', + content: [ + { + type: 'text', + data: '昨日上午北京道路车辆通行状况,9:00的峰值(1330)可能显示早高峰拥堵最严重时段,10:00后缓慢回落,可以得出如下折线图:', + }, + { + type: 'chart', + data: { + id: '13123', + chartType: 'line', + options: { + xAxis: { + type: 'category', + data: [ + '0:00', + '1:00', + '2:00', + '3:00', + '4:00', + '5:00', + '6:00', + '7:00', + '8:00', + '9:00', + '10:00', + '11:00', + '12:00', + ], + }, + yAxis: { + axisLabel: { inside: false }, + }, + series: [ + { + data: [820, 932, 901, 934, 600, 500, 700, 900, 1330, 1320, 1200, 1300, 1100], + type: 'line', + }, + ], + }, + }, + }, + ], +}; + +const ChartDemo = ({ data }) => ( +
+ +
+); + +const customRenderConfig = { + chart: (content) => ({ + slotName: `${content.type}-${content.data.id}`, + }), +}; + +export default function ChatMessageExample() { + return ( + + + {/* 自定义渲染-植入插槽 */} + {message.content.map(({ type, data }) => { + switch (type) { + case 'chart': + return ( +
+ +
+ ); + } + return null; + })} +
+
+ ); +} diff --git a/packages/components/chat-message/_example/status.tsx b/packages/components/chat-message/_example/status.tsx new file mode 100644 index 0000000000..8e2c28e40e --- /dev/null +++ b/packages/components/chat-message/_example/status.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { AIMessage, ChatMessage, Divider, Space, Select, TdChatLoadingProps } from 'tdesign-react'; + +const messages: Record = { + loading: { + id: '11111', + role: 'assistant', + status: 'pending', + }, + error: { + id: '22222', + role: 'assistant', + status: 'error', + }, +}; + +export default function ChatMessageExample() { + const [animation, setAnimation] = useState('skeleton'); + + return ( + + 消息加载状态 + } placeholder="请输入账户名" /> + + + } clearable={true} placeholder="请输入密码" /> + + + + + + + ); +} From 06a610df31387569464841683693a5730a6e47f6 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Thu, 15 May 2025 20:42:09 +0800 Subject: [PATCH 053/228] feat(chatbot): image demo --- .../chat-sender/_example/attachment.tsx | 1 + .../components/chatbot/_example/basic.tsx | 100 ++--- packages/components/chatbot/_example/code.tsx | 42 +-- .../components/chatbot/_example/custom.tsx | 12 +- .../components/chatbot/_example/image.tsx | 344 ++++++++++++++++++ .../components/chatbot/_example/research.tsx | 262 +++++++++++++ packages/components/chatbot/chatbot.md | 12 +- server/chat/data/chart.js | 4 +- server/chat/data/image.js | 48 +++ server/chat/ssemock.js | 8 +- 10 files changed, 745 insertions(+), 88 deletions(-) create mode 100644 packages/components/chatbot/_example/image.tsx create mode 100644 packages/components/chatbot/_example/research.tsx create mode 100644 server/chat/data/image.js diff --git a/packages/components/chat-sender/_example/attachment.tsx b/packages/components/chat-sender/_example/attachment.tsx index b082b32a70..28768df206 100644 --- a/packages/components/chat-sender/_example/attachment.tsx +++ b/packages/components/chat-sender/_example/attachment.tsx @@ -60,6 +60,7 @@ const ChatSenderExample = () => { file.name === newFile.name ? { ...file, + url: 'https://tdesign.gtimg.com/site/avatar.jpg', // mock返回的图片地址 status: 'success', description: '上传成功', } diff --git a/packages/components/chatbot/_example/basic.tsx b/packages/components/chatbot/_example/basic.tsx index 687f2ea90d..44b57c6ffb 100644 --- a/packages/components/chatbot/_example/basic.tsx +++ b/packages/components/chatbot/_example/basic.tsx @@ -1,6 +1,13 @@ import React, { useEffect, useRef, useState } from 'react'; import { InternetIcon } from 'tdesign-icons-react'; -import type { SSEChunkData, AIMessageContent, RequestParams, ChatMessagesData, ChatServiceConfig } from 'tdesign-react'; +import type { + SSEChunkData, + AIMessageContent, + TdChatMessageConfigItem, + RequestParams, + ChatMessagesData, + ChatServiceConfig, +} from 'tdesign-react'; import { Button, ChatBot, Space, type TdChatbotApi } from 'tdesign-react'; // 默认初始化消息 @@ -37,52 +44,58 @@ export default function chatSample() { const [activeR1, setR1Active] = useState(false); const [activeSearch, setSearchActive] = useState(false); const reqParamsRef = useRef<{ think: boolean; search: boolean }>({ think: false, search: false }); - const [thinkCollapse, setThinkCollapse] = useState(false); // 消息属性配置 - const messageProps = { - user: { - variant: 'base', - placement: 'right', - avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', - }, - assistant: { - placement: 'left', - actions: ['replay', 'copy', 'good', 'bad'], - handleActions: { - // 处理消息操作回调 - good: async ({ message, active }) => { - // 点赞 - console.log('点赞', message, active); - }, - bad: async ({ message, active }) => { - // 点踩 - console.log('点踩', message, active); - }, - replay: ({ message, active }) => { - console.log('自定义重新回复', message, active); - chatRef?.current?.regenerate(); - }, - searchItem: ({ content, event }) => { - event.preventDefault(); - console.log('点击搜索条目', content); - }, - suggestion: ({ content }) => { - console.log('点击建议问题', content); - // 点建议问题自动填入输入框 - chatRef?.current?.addPrompt(content.prompt); - // 也可以点建议问题直接发送消息 - // chatRef?.current?.sendUserMessage({ prompt: content.prompt }); + const messageProps = (msg: ChatMessagesData): TdChatMessageConfigItem => { + const { role, content } = msg; + // 假设只有单条thinking + const thinking = content.find((item) => item.type === 'thinking'); + if (role === 'user') { + return { + variant: 'base', + placement: 'right', + avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', + }; + } + if (role === 'assistant') { + return { + placement: 'left', + actions: ['replay', 'copy', 'good', 'bad'], + handleActions: { + // 处理消息操作回调 + good: async ({ message, active }) => { + // 点赞 + console.log('点赞', message, active); + }, + bad: async ({ message, active }) => { + // 点踩 + console.log('点踩', message, active); + }, + replay: ({ message, active }) => { + console.log('自定义重新回复', message, active); + chatRef?.current?.regenerate(); + }, + searchItem: ({ content, event }) => { + event.preventDefault(); + console.log('点击搜索条目', content); + }, + suggestion: ({ content }) => { + console.log('点击建议问题', content); + // 点建议问题自动填入输入框 + chatRef?.current?.addPrompt(content.prompt); + // 也可以点建议问题直接发送消息 + // chatRef?.current?.sendUserMessage({ prompt: content.prompt }); + }, }, - }, - // 内置的消息渲染配置 - chatContentProps: { - thinking: { - maxHeight: 100, - collapsed: thinkCollapse, + // 内置的消息渲染配置 + chatContentProps: { + thinking: { + maxHeight: 100, // 思考框最大高度,超过会自动滚动 + collapsed: thinking?.status === 'complete', // 是否折叠,这里设置内容输出完成后折叠 + }, }, - }, - }, + }; + } }; // 聊天服务配置 @@ -125,7 +138,6 @@ export default function chatSample() { }; // 正文 case 'text': - setThinkCollapse(true); return { type: 'markdown', data: rest?.msg || '', diff --git a/packages/components/chatbot/_example/code.tsx b/packages/components/chatbot/_example/code.tsx index 60aee17a88..ea5ee88614 100644 --- a/packages/components/chatbot/_example/code.tsx +++ b/packages/components/chatbot/_example/code.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { InternetIcon } from 'tdesign-icons-react'; +import React, { useRef } from 'react'; import type { SSEChunkData, TdChatMessageConfig, @@ -7,15 +6,7 @@ import type { RequestParams, ChatServiceConfig, } from 'tdesign-react'; -import { - Card, - ChatBot, - ChatMessagesData, - TdChatCustomRenderConfig, - DialogPlugin, - type TdChatbotApi, - Space, -} from 'tdesign-react'; +import { Card, ChatBot, ChatMessagesData, DialogPlugin, type TdChatbotApi, Space } from 'tdesign-react'; import Login from './components/login'; // 默认初始化消息 @@ -51,7 +42,6 @@ const PreviewCard = ({ header, desc, loading, code }) => { // 复制生成的代码 const copyHandler = async () => { try { - console.log('====code', code); const codeBlocks = Array.from(code.matchAll(/```(?:jsx|javascript)?\n([\s\S]*?)```/g)).map((match) => match[1].trim(), ); @@ -94,12 +84,6 @@ const PreviewCard = ({ header, desc, loading, code }) => { ); }; -const customRenderConfig: TdChatCustomRenderConfig = { - preview: (content) => ({ - slotName: `${content.type}-${content.data.id}`, - }), -}; - export default function chatSample() { const chatRef = useRef(null); const [mockMessage, setMockMessage] = React.useState(mockData); @@ -112,7 +96,6 @@ export default function chatSample() { avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', }, assistant: { - customRenderConfig, actions: ['replay', 'good', 'bad'], handleActions: { // 处理消息操作回调 @@ -198,26 +181,15 @@ export default function chatSample() { }, }; - useEffect(() => { - if (!chatRef.current) { - return; - } - const chat = chatRef.current; - const update = (e: CustomEvent) => { - setMockMessage(e.detail); - }; - chat.addEventListener('message_change', update); - return () => { - chat.removeEventListener('message_change', update); - }; - }, []); - return (
{ + setMockMessage(e.detail); + }} senderProps={{ defaultValue: '使用tdesign组件库实现一个登录表单的例子', placeholder: '有问题,尽管问~ Enter 发送,Shift+Enter 换行', @@ -227,12 +199,12 @@ export default function chatSample() { {/* 自定义消息体渲染-植入插槽 */} {mockMessage ?.map((msg) => - msg.content.map((item) => { + msg.content.map((item, index) => { switch (item.type) { // 示例:代码运行结果预览 case 'preview': return ( -
+
(
); -const initMessage = [ +const initMessage: ChatMessagesData[] = [ { id: '123', - role: 'user', + role: 'assistant', content: [ { type: 'text', - data: '北京今天早晚高峰交通情况如何,需要分别给出曲线图表示每个时段', + status: 'complete', + data: '欢迎使用TDesign智能图表分析助手,请输入你的问题', }, ], }, @@ -141,8 +142,11 @@ export default function ChatBotReact() { { setMockMessage(e.detail); diff --git a/packages/components/chatbot/_example/image.tsx b/packages/components/chatbot/_example/image.tsx new file mode 100644 index 0000000000..8b4b464537 --- /dev/null +++ b/packages/components/chatbot/_example/image.tsx @@ -0,0 +1,344 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { BrowseIcon, Filter3Icon, ImageAddIcon, Transform1Icon } from 'tdesign-icons-react'; +import type { + SSEChunkData, + AIMessageContent, + TdChatMessageConfigItem, + RequestParams, + ChatMessagesData, + ChatServiceConfig, + TdAttachmentItem, + TdChatSenderParams, + UploadFile, + ChatRequestParams, +} from 'tdesign-react'; +import { Button, ChatBot, Dropdown, Space, Image, type TdChatbotApi, ImageViewer, Skeleton } from 'tdesign-react'; + +const RatioOptions = [ + { + content: '1:1 头像', + value: 1, + }, + { + content: '2:3 自拍', + value: 2 / 3, + }, + { + content: '4:3 插画', + value: 4 / 3, + }, + { + content: '9:16 人像', + value: 9 / 16, + }, + { + content: '16:9 风景', + value: 16 / 9, + }, +]; + +const StyleOptions = [ + { + content: '人像摄影', + value: 'portrait', + }, + { + content: '卡通动漫', + value: 'cartoon', + }, + { + content: '风景', + value: 'landscape', + }, + { + content: '像素风', + value: 'pixel', + }, +]; + +// 默认初始化消息 +const mockData: ChatMessagesData[] = [ + { + id: '123', + role: 'assistant', + content: [ + { + type: 'text', + status: 'complete', + data: '欢迎使用TDesign智能生图助手,请先写下你的创意,可以上传参考图哦~', + }, + ], + }, +]; + +import type { ImageViewerProps } from 'tdesign-react'; + +// 自定义生图消息内容 +const BasicImageViewer = ({ images }) => { + if (images?.length === 0 || images?.every((img) => img === undefined)) { + return ; + } + + return ( + + {images.map((imgSrc, index) => { + const trigger: ImageViewerProps['trigger'] = ({ open }) => { + const mask = ( +
+ + 预览 + +
+ ); + + return ( + {'test'} + ); + }; + + return ; + })} +
+ ); +}; + +export default function chatSample() { + const chatRef = useRef(null); + const [ratio, setRatio] = useState(0); + const [style, setStyle] = useState(''); + const reqParamsRef = useRef<{ ratio: number; style: string; file?: string }>({ ratio: 0, style: '' }); + const [files, setFiles] = useState([]); + const [mockMessage, setMockMessage] = React.useState(mockData); + // 消息属性配置 + const messageProps = (msg: ChatMessagesData): TdChatMessageConfigItem => { + const { role, content } = msg; + // 假设只有单条thinking + const thinking = content.find((item) => item.type === 'thinking'); + if (role === 'user') { + return { + variant: 'base', + placement: 'right', + avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', + }; + } + if (role === 'assistant') { + return { + placement: 'left', + actions: ['good', 'bad'], + handleActions: { + // 处理消息操作回调 + good: async ({ message, active }) => { + // 点赞 + console.log('点赞', message, active); + }, + bad: async ({ message, active }) => { + // 点踩 + console.log('点踩', message, active); + }, + }, + }; + } + }; + + // 聊天服务配置 + const chatServiceConfig: ChatServiceConfig = { + // 对话服务地址 + endpoint: 'http://localhost:3000/sse/normal', + stream: true, + // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) + onComplete: (aborted: boolean, params: RequestInit) => { + console.log('onComplete', aborted, params); + }, + // 流式对话过程中出错业务自定义行为 + onError: (err: Error | Response) => { + console.error('Chatservice Error:', err); + }, + // 流式对话过程中用户主动结束对话业务自定义行为 + onAbort: async () => {}, + // 自定义流式数据结构解析 + onMessage: (chunk: SSEChunkData): AIMessageContent => { + const { type, ...rest } = chunk.data; + switch (type) { + // 图片列表预览(自定义渲染) + case 'image': + return { + type: 'imageview', + status: 'complete', + data: JSON.parse(rest.content), + }; + // 正文 + case 'text': + return { + type: 'markdown', + data: rest?.msg || '', + }; + } + }, + // 自定义请求参数 + onRequest: (innerParams: RequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'tdesign-chat', + prompt, + image: true, + ...reqParamsRef.current, + }), + }; + }, + }; + + // 选中文件 + const onAttachClick = () => { + chatRef.current?.selectFile(); + }; + // 文件上传 + const onFileSelect = (e: CustomEvent) => { + // 添加新文件并模拟上传进度 + let newFile = { + ...e.detail[0], + name: e.detail[0].name, + status: 'progress' as UploadFile['status'], + description: '上传中', + }; + + setFiles((prev) => [newFile, ...prev]); + + setTimeout(() => { + setFiles((prevState) => + prevState.map((file) => + file.name === newFile.name + ? { + ...file, + url: 'https://tdesign.gtimg.com/site/avatar.jpg', // mock返回的图片地址 + status: 'success', + description: '上传成功', + } + : file, + ), + ); + }, 1000); + }; + // 移除文件回调 + const onFileRemove = (e: CustomEvent) => { + setFiles(e.detail); + }; + + // 发送用户消息回调,这里可以自定义修改返回的prompt + const onSend = (e: CustomEvent): ChatRequestParams => { + const { value, attachments } = e.detail; + return { + attachments, + prompt: `${value},要求比例:${ratio === 0 ? '1:1' : ratio}, 风格:${ + style ? StyleOptions.filter(({ value }) => value === style)[0].content : '默认风格' + }`, + }; + }; + + const switchRatio = (data) => { + setRatio(data.value); + }; + const switchStyle = (data) => { + setStyle(data.value); + }; + + useEffect(() => { + reqParamsRef.current = { + ratio, + style, + file: 'https://tdesign.gtimg.com/site/avatar.jpg', + }; + }, [ratio, style]); + + return ( +
+ { + setMockMessage(e.detail); + }} + > + {mockMessage + ?.map((msg) => + msg.content.map((item, index) => { + switch (item.type) { + // 示例:图片消息体 + case 'imageview': + return ( + // slot名这里必须保证唯一性 +
+ img?.url)} /> +
+ ); + } + return null; + }), + ) + .flat()} + {/* 自定义输入框底部区域slot,可以增加模型选项 */} +
+ + + + + + + + + +
+
+
+ ); +} diff --git a/packages/components/chatbot/_example/research.tsx b/packages/components/chatbot/_example/research.tsx new file mode 100644 index 0000000000..b11a163814 --- /dev/null +++ b/packages/components/chatbot/_example/research.tsx @@ -0,0 +1,262 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ChevronDownIcon, Filter3Icon, ImageAddIcon, InternetIcon, Transform1Icon } from 'tdesign-icons-react'; +import type { + SSEChunkData, + AIMessageContent, + TdChatMessageConfigItem, + RequestParams, + ChatMessagesData, + ChatServiceConfig, +} from 'tdesign-react'; +import { Button, ChatBot, Dropdown, Space, Tooltip, Tag, type TdChatbotApi } from 'tdesign-react'; + +const RatioOptions = [ + { + content: '1:1 头像', + value: 1, + }, + { + content: '2:3 自拍', + value: 2 / 3, + }, + { + content: '4:3 插画', + value: 4 / 3, + }, + { + content: '9:16 人像', + value: 9 / 16, + }, + { + content: '16:9 风景', + value: 16 / 9, + }, +]; + +const StyleOptions = [ + { + content: '人像摄影', + value: 'portrait', + }, + { + content: '卡通动漫', + value: 'cartoon', + }, + { + content: '风景', + value: 'landscape', + }, + { + content: '像素风', + value: 'pixel', + }, +]; + +// 默认初始化消息 +const mockData: ChatMessagesData[] = [ + { + id: '123', + role: 'assistant', + content: [ + { + type: 'text', + status: 'complete', + data: '欢迎使用TDesign Chatbot智能助手,你可以这样问我:', + }, + { + type: 'suggestion', + status: 'complete', + data: [ + { + title: '南极的自动提款机叫什么名字', + prompt: '南极的自动提款机叫什么名字?', + }, + { + title: '南极自动提款机在哪里', + prompt: '南极自动提款机在哪里', + }, + ], + }, + ], + }, +]; + +export default function chatSample() { + const chatRef = useRef(null); + const [ratio, setRatio] = useState(0); + const [style, setStyle] = useState(''); + const reqParamsRef = useRef<{ ratio: number; style: string }>({ ratio: 0, style: '' }); + + // 消息属性配置 + const messageProps = (msg: ChatMessagesData): TdChatMessageConfigItem => { + const { role, content } = msg; + // 假设只有单条thinking + const thinking = content.find((item) => item.type === 'thinking'); + if (role === 'user') { + return { + variant: 'base', + placement: 'right', + avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', + }; + } + if (role === 'assistant') { + return { + placement: 'left', + actions: ['replay', 'copy', 'good', 'bad'], + handleActions: { + // 处理消息操作回调 + good: async ({ message, active }) => { + // 点赞 + console.log('点赞', message, active); + }, + bad: async ({ message, active }) => { + // 点踩 + console.log('点踩', message, active); + }, + replay: ({ message, active }) => { + console.log('自定义重新回复', message, active); + chatRef?.current?.regenerate(); + }, + searchItem: ({ content, event }) => { + event.preventDefault(); + console.log('点击搜索条目', content); + }, + suggestion: ({ content }) => { + console.log('点击建议问题', content); + // 点建议问题自动填入输入框 + chatRef?.current?.addPrompt(content.prompt); + // 也可以点建议问题直接发送消息 + // chatRef?.current?.sendUserMessage({ prompt: content.prompt }); + }, + }, + // 内置的消息渲染配置 + chatContentProps: { + thinking: { + maxHeight: 100, // 思考框最大高度,超过会自动滚动 + collapsed: thinking?.status === 'complete', // 是否折叠,这里设置内容输出完成后折叠 + }, + }, + }; + } + }; + + // 聊天服务配置 + const chatServiceConfig: ChatServiceConfig = { + // 对话服务地址 + endpoint: 'http://localhost:3000/sse/normal', + stream: true, + // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) + onComplete: (aborted: boolean, params: RequestInit) => { + console.log('onComplete', aborted, params); + }, + // 流式对话过程中出错业务自定义行为 + onError: (err: Error | Response) => { + console.error('Chatservice Error:', err); + }, + // 流式对话过程中用户主动结束对话业务自定义行为 + onAbort: async () => {}, + // 自定义流式数据结构解析 + onMessage: (chunk: SSEChunkData): AIMessageContent => { + const { type, ...rest } = chunk.data; + switch (type) { + case 'search': + // 搜索 + return { + type: 'search', + data: { + title: rest.title || `搜索到${rest?.docs.length}条内容`, + references: rest?.content, + }, + }; + // 思考 + case 'think': + return { + type: 'thinking', + status: (status) => (/耗时/.test(rest?.title) ? 'complete' : status), + data: { + title: rest.title || '深度思考中', + text: rest.content || '', + }, + }; + // 正文 + case 'text': + return { + type: 'markdown', + data: rest?.msg || '', + }; + } + }, + // 自定义请求参数 + onRequest: (innerParams: RequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'tdesign-chat', + prompt, + ...reqParamsRef.current, + }), + }; + }, + }; + + const onAttachClick = () => { + chatRef.current?.selectFile(); + }; + + const switchRatio = (data) => { + setRatio(data.value); + }; + const switchStyle = (data) => { + setStyle(data.value); + }; + + useEffect(() => { + reqParamsRef.current = { + ratio, + style, + }; + }, [ratio, style]); + + return ( +
+ + {/* 自定义输入框底部区域slot,可以增加模型选项 */} +
+ + + + + + + + + + + +
+
+
+ ); +} diff --git a/packages/components/chatbot/chatbot.md b/packages/components/chatbot/chatbot.md index 20fdddf8fe..18df301be4 100644 --- a/packages/components/chatbot/chatbot.md +++ b/packages/components/chatbot/chatbot.md @@ -39,10 +39,18 @@ spline: navigation 以下再通过几个常见的业务场景,展示下如何使用 `Chatbot` 组件 ### 代码助手 +通过使用tdesign开发登录框组件的案例,演示了使用Chatbot搭建简单的代码助手场景,该示例你可以了解到如何按需开启**markdown渲染代码块**,如何**自定义实现代码预览** {{ code }} -### 多模态交付 -{{ multimedia }} +### 文档理解 +以下案例演示了使用Chatbot搭建简单的文件理解应用,通过该示例你可以了解到如何**使用附件区域**,同时演示了**附件类型的内容渲染** +{{ research }} + +### 图像生成 +以下案例演示了使用Chatbot搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**图片类型的内容渲染** + +{{ image }} + ### 自主任务规划 {{ agent }} diff --git a/server/chat/data/chart.js b/server/chat/data/chart.js index 94451883d2..ee3105b183 100644 --- a/server/chat/data/chart.js +++ b/server/chat/data/chart.js @@ -4,7 +4,7 @@ const chunks = [ { type: 'text', msg: '北京道路' }, { type: 'text', msg: '车辆' }, { type: 'text', msg: '通行状况' }, - { type: 'text', msg: '9:00的峰值(1330),' }, + { type: 'text', msg: '9:00的峰值(1320),' }, { type: 'text', msg: '可能显示早高峰拥堵最严重时段' }, { type: 'text', msg: '10:00后缓慢回落,' }, { type: 'text', msg: '可以得出如下折线图:' }, @@ -37,7 +37,7 @@ const chunks = [ }, series: [ { - data: [820, 932, 901, 934, 600, 500, 700, 900, 1330, 1320, 1200, 1300, 1100], + data: [500, 402, 382, 434, 560, 630, 720, 980, 1230, 1320, 1200, 1300, 1100], type: 'line', }, ], diff --git a/server/chat/data/image.js b/server/chat/data/image.js new file mode 100644 index 0000000000..6a5d7b7b02 --- /dev/null +++ b/server/chat/data/image.js @@ -0,0 +1,48 @@ +module.exports = [ + { type: 'text', msg: '接下来我将生成符合要求的图片' }, + { + type: 'image', + content: '[{"progress": 0},{"progress": 0}, {"progress": 0},{"progress": 0}]', + }, + { + type: 'image', + content: '[{"progress": 10},{"progress": 10}, {"progress": 10},{"progress": 10}]', + }, + { + type: 'image', + content: '[{"progress": 20},{"progress": 20}, {"progress": 20},{"progress": 20}]', + }, + { + type: 'image', + content: '[{"progress": 30},{"progress": 30}, {"progress": 30},{"progress": 30}]', + }, + { + type: 'image', + content: '[{"progress": 40},{"progress": 40}, {"progress": 40},{"progress": 40}]', + }, + { + type: 'image', + content: '[{"progress": 50},{"progress": 50}, {"progress": 50},{"progress": 50}]', + }, + { + type: 'image', + content: '[{"progress": 60},{"progress": 60}, {"progress": 60},{"progress": 60}]', + }, + { + type: 'image', + content: '[{"progress": 70},{"progress": 70}, {"progress": 70},{"progress": 70}]', + }, + { + type: 'image', + content: '[{"progress": 80},{"progress": 80}, {"progress": 80},{"progress": 80}]', + }, + { + type: 'image', + content: '[{"progress": 90},{"progress": 90}, {"progress": 90},{"progress": 90}]', + }, + { + type: 'image', + content: + '[{"url":"https://tdesign.gtimg.com/demo/demo-image-1.png","format":"png","width":1204,"height":1024,"size":1032},{"url":"https://tdesign.gtimg.com/demo/demo-image-2.png","format":"png","width":1204,"height":1024,"size":1032},{"url":"https://tdesign.gtimg.com/demo/demo-image-3.png","format":"png","width":1204,"height":1024,"size":1032}]', + }, +]; diff --git a/server/chat/ssemock.js b/server/chat/ssemock.js index 0468a06a29..9c9fd2d901 100644 --- a/server/chat/ssemock.js +++ b/server/chat/ssemock.js @@ -3,6 +3,7 @@ const express = require('express'); const chunks = require('./data/normal'); const chunksChart = require('./data/chart'); const chunksCode = require('./data/code'); +const chunksImage = require('./data/image'); const app = express(); app.use(cors()); @@ -60,7 +61,7 @@ app.post('/sse/normal', (req, res) => { setSSEHeaders(res); let mockdata = chunks; - const { think = false, search = false, chart = false, code = false } = req.body; + const { think = false, search = false, chart = false, code = false, image = false } = req.body; if (chart) { mockdata = chunksChart; } @@ -68,6 +69,11 @@ app.post('/sse/normal', (req, res) => { if (code) { mockdata = chunksCode; } + + if (image) { + mockdata = chunksImage; + } + // 根据参数过滤不需要的chunk类型 const filteredChunks = mockdata.filter((chunk) => { if (!think && chunk.type === 'think') return false; From c1537b495ffefef2edf3a4804bbe60c059d5e403 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Thu, 15 May 2025 21:02:36 +0800 Subject: [PATCH 054/228] feat(chatbot): demo refine --- .../chat-sender/_example/attachment.tsx | 2 +- .../components/chatbot/_example/image.tsx | 51 ++-- .../components/chatbot/_example/research.tsx | 253 ++++++------------ packages/components/chatbot/chatbot.md | 2 +- 4 files changed, 105 insertions(+), 203 deletions(-) diff --git a/packages/components/chat-sender/_example/attachment.tsx b/packages/components/chat-sender/_example/attachment.tsx index 28768df206..43e1792303 100644 --- a/packages/components/chat-sender/_example/attachment.tsx +++ b/packages/components/chat-sender/_example/attachment.tsx @@ -60,7 +60,7 @@ const ChatSenderExample = () => { file.name === newFile.name ? { ...file, - url: 'https://tdesign.gtimg.com/site/avatar.jpg', // mock返回的图片地址 + url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'success', description: '上传成功', } diff --git a/packages/components/chatbot/_example/image.tsx b/packages/components/chatbot/_example/image.tsx index 8b4b464537..4f64b4a0e7 100644 --- a/packages/components/chatbot/_example/image.tsx +++ b/packages/components/chatbot/_example/image.tsx @@ -65,7 +65,7 @@ const mockData: ChatMessagesData[] = [ { type: 'text', status: 'complete', - data: '欢迎使用TDesign智能生图助手,请先写下你的创意,可以上传参考图哦~', + data: '欢迎使用TDesign智能生图助手,请先写下你的创意,可以试试上传参考图哦~', }, ], }, @@ -133,35 +133,29 @@ export default function chatSample() { const reqParamsRef = useRef<{ ratio: number; style: string; file?: string }>({ ratio: 0, style: '' }); const [files, setFiles] = useState([]); const [mockMessage, setMockMessage] = React.useState(mockData); + // 消息属性配置 - const messageProps = (msg: ChatMessagesData): TdChatMessageConfigItem => { - const { role, content } = msg; - // 假设只有单条thinking - const thinking = content.find((item) => item.type === 'thinking'); - if (role === 'user') { - return { - variant: 'base', - placement: 'right', - avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', - }; - } - if (role === 'assistant') { - return { - placement: 'left', - actions: ['good', 'bad'], - handleActions: { - // 处理消息操作回调 - good: async ({ message, active }) => { - // 点赞 - console.log('点赞', message, active); - }, - bad: async ({ message, active }) => { - // 点踩 - console.log('点踩', message, active); - }, + const messageProps = { + user: { + variant: 'base', + placement: 'right', + avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', + }, + assistant: { + placement: 'left', + actions: ['good', 'bad'], + handleActions: { + // 处理消息操作回调 + good: async ({ message, active }) => { + // 点赞 + console.log('点赞', message, active); }, - }; - } + bad: async ({ message, active }) => { + // 点踩 + console.log('点踩', message, active); + }, + }, + }, }; // 聊天服务配置 @@ -255,6 +249,7 @@ export default function chatSample() { // 发送用户消息回调,这里可以自定义修改返回的prompt const onSend = (e: CustomEvent): ChatRequestParams => { const { value, attachments } = e.detail; + setFiles([]); // 清除掉附件区域 return { attachments, prompt: `${value},要求比例:${ratio === 0 ? '1:1' : ratio}, 风格:${ diff --git a/packages/components/chatbot/_example/research.tsx b/packages/components/chatbot/_example/research.tsx index b11a163814..f24681a472 100644 --- a/packages/components/chatbot/_example/research.tsx +++ b/packages/components/chatbot/_example/research.tsx @@ -1,56 +1,14 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { ChevronDownIcon, Filter3Icon, ImageAddIcon, InternetIcon, Transform1Icon } from 'tdesign-icons-react'; +import React, { useRef, useState } from 'react'; import type { SSEChunkData, AIMessageContent, - TdChatMessageConfigItem, - RequestParams, ChatMessagesData, ChatServiceConfig, + TdAttachmentItem, + UploadFile, + ChatRequestParams, } from 'tdesign-react'; -import { Button, ChatBot, Dropdown, Space, Tooltip, Tag, type TdChatbotApi } from 'tdesign-react'; - -const RatioOptions = [ - { - content: '1:1 头像', - value: 1, - }, - { - content: '2:3 自拍', - value: 2 / 3, - }, - { - content: '4:3 插画', - value: 4 / 3, - }, - { - content: '9:16 人像', - value: 9 / 16, - }, - { - content: '16:9 风景', - value: 16 / 9, - }, -]; - -const StyleOptions = [ - { - content: '人像摄影', - value: 'portrait', - }, - { - content: '卡通动漫', - value: 'cartoon', - }, - { - content: '风景', - value: 'landscape', - }, - { - content: '像素风', - value: 'pixel', - }, -]; +import { ChatBot, type TdChatbotApi } from 'tdesign-react'; // 默认初始化消息 const mockData: ChatMessagesData[] = [ @@ -61,21 +19,7 @@ const mockData: ChatMessagesData[] = [ { type: 'text', status: 'complete', - data: '欢迎使用TDesign Chatbot智能助手,你可以这样问我:', - }, - { - type: 'suggestion', - status: 'complete', - data: [ - { - title: '南极的自动提款机叫什么名字', - prompt: '南极的自动提款机叫什么名字?', - }, - { - title: '南极自动提款机在哪里', - prompt: '南极自动提款机在哪里', - }, - ], + data: '欢迎使用TDesign文档阅读助手,请先上传你需要识别和理解的文件吧~', }, ], }, @@ -83,61 +27,30 @@ const mockData: ChatMessagesData[] = [ export default function chatSample() { const chatRef = useRef(null); - const [ratio, setRatio] = useState(0); - const [style, setStyle] = useState(''); - const reqParamsRef = useRef<{ ratio: number; style: string }>({ ratio: 0, style: '' }); + const [files, setFiles] = useState([]); // 消息属性配置 - const messageProps = (msg: ChatMessagesData): TdChatMessageConfigItem => { - const { role, content } = msg; - // 假设只有单条thinking - const thinking = content.find((item) => item.type === 'thinking'); - if (role === 'user') { - return { - variant: 'base', - placement: 'right', - avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', - }; - } - if (role === 'assistant') { - return { - placement: 'left', - actions: ['replay', 'copy', 'good', 'bad'], - handleActions: { - // 处理消息操作回调 - good: async ({ message, active }) => { - // 点赞 - console.log('点赞', message, active); - }, - bad: async ({ message, active }) => { - // 点踩 - console.log('点踩', message, active); - }, - replay: ({ message, active }) => { - console.log('自定义重新回复', message, active); - chatRef?.current?.regenerate(); - }, - searchItem: ({ content, event }) => { - event.preventDefault(); - console.log('点击搜索条目', content); - }, - suggestion: ({ content }) => { - console.log('点击建议问题', content); - // 点建议问题自动填入输入框 - chatRef?.current?.addPrompt(content.prompt); - // 也可以点建议问题直接发送消息 - // chatRef?.current?.sendUserMessage({ prompt: content.prompt }); - }, + const messageProps = { + user: { + variant: 'base', + placement: 'right', + avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', + }, + assistant: { + placement: 'left', + actions: ['good', 'bad'], + handleActions: { + // 处理消息操作回调 + good: async ({ message, active }) => { + // 点赞 + console.log('点赞', message, active); }, - // 内置的消息渲染配置 - chatContentProps: { - thinking: { - maxHeight: 100, // 思考框最大高度,超过会自动滚动 - collapsed: thinking?.status === 'complete', // 是否折叠,这里设置内容输出完成后折叠 - }, + bad: async ({ message, active }) => { + // 点踩 + console.log('点踩', message, active); }, - }; - } + }, + }, }; // 聊天服务配置 @@ -159,24 +72,12 @@ export default function chatSample() { onMessage: (chunk: SSEChunkData): AIMessageContent => { const { type, ...rest } = chunk.data; switch (type) { - case 'search': - // 搜索 - return { - type: 'search', - data: { - title: rest.title || `搜索到${rest?.docs.length}条内容`, - references: rest?.content, - }, - }; - // 思考 - case 'think': + // 图片列表预览(自定义渲染) + case 'image': return { - type: 'thinking', - status: (status) => (/耗时/.test(rest?.title) ? 'complete' : status), - data: { - title: rest.title || '深度思考中', - text: rest.content || '', - }, + type: 'imageview', + status: 'complete', + data: JSON.parse(rest.content), }; // 正文 case 'text': @@ -187,8 +88,8 @@ export default function chatSample() { } }, // 自定义请求参数 - onRequest: (innerParams: RequestParams) => { - const { prompt } = innerParams; + onRequest: (innerParams: ChatRequestParams) => { + const { prompt, attachments } = innerParams; return { headers: { 'Content-Type': 'application/json', @@ -197,29 +98,49 @@ export default function chatSample() { body: JSON.stringify({ uid: 'tdesign-chat', prompt, - ...reqParamsRef.current, + files: attachments, + docs: true, }), }; }, }; - const onAttachClick = () => { - chatRef.current?.selectFile(); - }; + // 文件上传 + const onFileSelect = (e: CustomEvent) => { + // 添加新文件并模拟上传进度 + let newFile = { + ...e.detail[0], + name: e.detail[0].name, + status: 'progress' as UploadFile['status'], + description: '上传中', + }; - const switchRatio = (data) => { - setRatio(data.value); + setFiles((prev) => [newFile, ...prev]); + + setTimeout(() => { + setFiles((prevState) => + prevState.map((file) => + file.name === newFile.name + ? { + ...file, + url: 'https://tdesign.gtimg.com/site/demo.docx', // mock返回的文件地址 + status: 'success', + description: '上传成功', + } + : file, + ), + ); + }, 1000); }; - const switchStyle = (data) => { - setStyle(data.value); + + // 移除文件回调 + const onFileRemove = (e: CustomEvent) => { + setFiles(e.detail); }; - useEffect(() => { - reqParamsRef.current = { - ratio, - style, - }; - }, [ratio, style]); + const onSend = () => { + setFiles([]); // 清除掉附件区域 + }; return (
@@ -228,35 +149,21 @@ export default function chatSample() { messages={mockData} messageProps={messageProps} senderProps={{ - placeholder: '有问题,尽管问~ Enter 发送,Shift+Enter 换行', + placeholder: '上传你需要识别和理解的文件吧~', + actions: (preset) => preset, + uploadProps: { + multiple: true, + }, + attachmentsProps: { + items: files, + overflow: 'scrollX', + }, + onSend, + onFileSelect, + onFileRemove, }} chatServiceConfig={chatServiceConfig} - > - {/* 自定义输入框底部区域slot,可以增加模型选项 */} -
- - - - - - - - - - - -
- + >
); } diff --git a/packages/components/chatbot/chatbot.md b/packages/components/chatbot/chatbot.md index 18df301be4..1114a298ae 100644 --- a/packages/components/chatbot/chatbot.md +++ b/packages/components/chatbot/chatbot.md @@ -47,7 +47,7 @@ spline: navigation {{ research }} ### 图像生成 -以下案例演示了使用Chatbot搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**图片类型的内容渲染** +以下案例演示了使用Chatbot搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** {{ image }} From f33688c13d3842053eed208e717071907b9771d9 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Thu, 15 May 2025 21:09:44 +0800 Subject: [PATCH 055/228] feat(chatbot): agent demo --- .../components/chatbot/_example/agent.tsx | 45 +------- server/chat/data/agent.js | 102 ++++++++++++++++++ server/chat/ssemock.js | 1 + 3 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 server/chat/data/agent.js diff --git a/packages/components/chatbot/_example/agent.tsx b/packages/components/chatbot/_example/agent.tsx index c993f96c9c..860be6bebe 100644 --- a/packages/components/chatbot/_example/agent.tsx +++ b/packages/components/chatbot/_example/agent.tsx @@ -57,56 +57,18 @@ declare module 'tdesign-react' { } >; } - - // 扩展允许的消息类型 - interface AIMessageContentOverrides { - type: 'agent' | 'search' | 'text' | 'markdown' | 'thinking' | 'image' | 'suggestion' | 'attachment'; - } } // 默认初始化消息 const mockData: ChatMessagesData[] = [ { id: '123', - role: 'user', - status: 'complete', - content: [ - { - type: 'text', - data: '请帮我做一个家庭聚会任务规划', - }, - ], - }, - { - id: '222', role: 'assistant', status: 'complete', content: [ { - type: 'agent', - state: 'agent_init', - id: '111111', - content: { - text: '家庭聚会规划任务已分解为3个执行阶段', - steps: [ - { - step: '① 餐饮方案', - agent_id: 'a1', - time: '2分钟', - status: 'finish', - tasks: [ - { type: 'command', text: '开始生成餐饮方案:正在分析用户饮食偏好...' }, - { type: 'command', text: '已筛选出3种高性价比菜单方案,正在进行营养匹配...' }, - { - type: 'result', - text: '🍴 推荐餐饮方案:主菜是香草烤鸡(无麸质),准备耗时45分钟;饮品是智能调酒机方案B,酒精浓度12%', - }, - ], - }, - { step: '② 设备调度', agent_id: 'a2', time: '3分钟' }, - { step: '③ 安全监测', agent_id: 'a3', time: '1分钟' }, - ], - }, + type: 'text', + data: '欢迎使用TDesign Agent家庭活动策划助手,请给我布置任务吧~', }, ], }, @@ -233,6 +195,9 @@ export default function ChatBotReact() { style={{ height: '100%' }} messages={mockData} messageProps={messageProps} + senderProps={{ + defaultValue: '请帮我做一个家庭聚会任务规划', + }} chatServiceConfig={chatServiceConfig} > {mockMessage diff --git a/server/chat/data/agent.js b/server/chat/data/agent.js new file mode 100644 index 0000000000..c428dc2665 --- /dev/null +++ b/server/chat/data/agent.js @@ -0,0 +1,102 @@ +module.exports = [ + { + type: 'agent', + state: 'agent_init', + id: '111111', + content: { + text: '家庭聚会规划任务已分解为3个执行阶段', + steps: [ + { step: '① 餐饮方案', agent_id: 'a1', time: '2分钟' }, + { step: '② 设备调度', agent_id: 'a2', time: '3分钟' }, + { step: '③ 安全监测', agent_id: 'a3', time: '1分钟' }, + ], + }, + }, + { + type: 'agent', + state: 'agent_update', + id: '222', + content: { + agent_id: 'a1', + text: '开始生成餐饮方案:正在分析用户饮食偏好...', + }, + }, + { + type: 'agent', + state: 'agent_update', + id: '333', + content: { + agent_id: 'a1', + text: '已筛选出3种高性价比菜单方案,正在进行营养匹配...', + }, + }, + { + type: 'agent', + state: 'agent_result', + id: '444', + content: { + agent_id: 'a1', + text: '🍴 推荐餐饮方案:主菜是香草烤鸡(无麸质),准备耗时45分钟;饮品是智能调酒机方案B,酒精浓度12%', + }, + }, + { + type: 'agent', + state: 'agent_finish', + id: '44455', + content: { + agent_id: 'a1', + }, + }, + { + type: 'agent', + state: 'agent_update', + id: '555', + content: { + agent_id: 'a2', + text: '设备调度中:已激活厨房智能设备...', + }, + }, + { + type: 'agent', + state: 'agent_result', + id: '666', + content: { + agent_id: 'a2', + text: '📱 设备调度方案:智能烤箱预热至180℃,倒计时09:15启动;环境调节至23℃,湿度55%', + }, + }, + { + type: 'agent', + state: 'agent_finish', + id: '4445566', + content: { + agent_id: 'a2', + }, + }, + { + type: 'agent', + state: 'agent_update', + id: '777', + content: { + agent_id: 'a3', + text: '安全巡检完成:未发现燃气泄漏风险', + }, + }, + { + type: 'agent', + state: 'agent_result', + id: '888', + content: { + agent_id: 'a3', + text: '所有智能体已完成协作', + }, + }, + { + type: 'agent', + state: 'agent_finish', + id: '444556677', + content: { + agent_id: 'a3', + }, + }, +]; diff --git a/server/chat/ssemock.js b/server/chat/ssemock.js index 9c9fd2d901..725a7d359d 100644 --- a/server/chat/ssemock.js +++ b/server/chat/ssemock.js @@ -4,6 +4,7 @@ const chunks = require('./data/normal'); const chunksChart = require('./data/chart'); const chunksCode = require('./data/code'); const chunksImage = require('./data/image'); +const agentChunks = require('./data/agent'); const app = express(); app.use(cors()); From e2f4c58d52bc952eefc724bbe5e83b8666df58ca Mon Sep 17 00:00:00 2001 From: carolin913 Date: Fri, 16 May 2025 11:54:53 +0800 Subject: [PATCH 056/228] feat(chatbot): image demo --- packages/components/chatbot/_example/image.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/chatbot/_example/image.tsx b/packages/components/chatbot/_example/image.tsx index 4f64b4a0e7..e477eed8bb 100644 --- a/packages/components/chatbot/_example/image.tsx +++ b/packages/components/chatbot/_example/image.tsx @@ -252,9 +252,9 @@ export default function chatSample() { setFiles([]); // 清除掉附件区域 return { attachments, - prompt: `${value},要求比例:${ratio === 0 ? '1:1' : ratio}, 风格:${ - style ? StyleOptions.filter(({ value }) => value === style)[0].content : '默认风格' - }`, + prompt: `${value},要求比例:${ + ratio === 0 ? '默认比例' : RatioOptions.filter(({ value }) => value === ratio)[0].content + }, 风格:${style ? StyleOptions.filter(({ value }) => value === style)[0].content : '默认风格'}`, }; }; From f70692af4db674e2d499d2a4acb812492d41e5e7 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Fri, 16 May 2025 12:26:17 +0800 Subject: [PATCH 057/228] feat(chatbot): defaultmessages --- packages/components/chat-sender/_example/custom.tsx | 1 + packages/components/chat-sender/_example/style.css | 1 - packages/components/chatbot/_example/agent.tsx | 2 +- packages/components/chatbot/_example/basic.tsx | 2 +- packages/components/chatbot/_example/code.tsx | 2 +- packages/components/chatbot/_example/custom.tsx | 2 +- packages/components/chatbot/_example/image.tsx | 3 +-- packages/components/chatbot/_example/research.tsx | 2 +- 8 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index cb13198a24..31b8836112 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -38,6 +38,7 @@ const ChatSenderExample = () => { .${styleId.current} { --td-text-color-placeholder: #DFE2E7; --td-bg-color-secondarycontainer: #fff; + --td-chat-input-background: #fff; } `; diff --git a/packages/components/chat-sender/_example/style.css b/packages/components/chat-sender/_example/style.css index edb85cc499..bb8d51eaed 100644 --- a/packages/components/chat-sender/_example/style.css +++ b/packages/components/chat-sender/_example/style.css @@ -1,4 +1,3 @@ :root { --td-text-color-placeholder: #DFE2E7; - --td-bg-color-secondarycontainer: #fff; } \ No newline at end of file diff --git a/packages/components/chatbot/_example/agent.tsx b/packages/components/chatbot/_example/agent.tsx index 860be6bebe..3e29844851 100644 --- a/packages/components/chatbot/_example/agent.tsx +++ b/packages/components/chatbot/_example/agent.tsx @@ -193,7 +193,7 @@ export default function ChatBotReact() { { setMockMessage(e.detail); diff --git a/packages/components/chatbot/_example/custom.tsx b/packages/components/chatbot/_example/custom.tsx index 63bfaed00c..a6b517644c 100644 --- a/packages/components/chatbot/_example/custom.tsx +++ b/packages/components/chatbot/_example/custom.tsx @@ -142,7 +142,7 @@ export default function ChatBotReact() { Date: Fri, 16 May 2025 18:15:21 +0800 Subject: [PATCH 058/228] feat(chatbot): agent demo --- .../components/chatbot/_example/agent.tsx | 117 +++++---- .../components/chatbot/_example/image.tsx | 3 +- .../components/chatbot/_example/research.tsx | 8 +- packages/components/chatbot/chatbot.md | 5 +- server/chat/data/agent.js | 230 +++++++++++++++--- server/chat/data/code.js | 3 +- 6 files changed, 275 insertions(+), 91 deletions(-) diff --git a/packages/components/chatbot/_example/agent.tsx b/packages/components/chatbot/_example/agent.tsx index 3e29844851..52544ec3d1 100644 --- a/packages/components/chatbot/_example/agent.tsx +++ b/packages/components/chatbot/_example/agent.tsx @@ -22,9 +22,9 @@ const customRenderConfig: TdChatCustomRenderConfig = { }), }; -const RenderAgent = ({ steps }) => { +const AgentTimeline = ({ steps }) => { return ( -
+
{steps.map((step) => ( ; } @@ -87,11 +100,6 @@ export default function ChatBotReact() { assistant: { placement: 'left', customRenderConfig, - chatContentProps: { - thinking: { - maxHeight: 100, - }, - }, }, }; @@ -151,41 +159,56 @@ export default function ChatBotReact() { if (!chatRef.current) { return; } - const chat = chatRef.current; - chat.registerMergeStrategy('agent', (newchunk, existing) => { - const newExisting = { ...existing }; - newExisting.content = { ...existing.content }; - newExisting.content.steps = [...existing.content.steps]; + // 此处增加自定义消息内容合并策略逻辑 + // 该示例agent类型结构比较复杂,根据任务步骤的state有不同的策略,组件内onMessage这里提供了的strategy无法满足,可以通过注册合并策略自行实现 + chatRef.current.registerMergeStrategy('agent', (newChunk, existing) => { + // 创建新对象避免直接修改原状态 + const updated = { + ...existing, + content: { + ...existing.content, + steps: [...existing.content.steps], + }, + }; + + const stepIndex = updated.content.steps.findIndex((step) => step.agent_id === newChunk.content.agent_id); - const stepIndex = newExisting.content.steps.findIndex((step) => step.agent_id === newchunk.content.agent_id); + if (stepIndex === -1) return updated; - if (stepIndex >= 0) { - const step = { ...newExisting.content.steps[stepIndex] }; + // 更新步骤信息 + const step = { + ...updated.content.steps[stepIndex], + tasks: [...(updated.content.steps[stepIndex].tasks || [])], + status: newChunk.state === 'finish' ? 'finish' : 'pending', + }; - if (['agent_update', 'agent_result'].includes(newchunk.state)) { - step.tasks = [...(step.tasks || [])]; + // 处理不同类型的新数据 + if (newChunk.state === 'command') { + // 新增每个步骤执行的命令 + step.tasks.push({ + type: 'command', + text: newChunk.content.text, + }); + } else if (newChunk.state === 'result') { + // 新增每个步骤执行的结论是流式输出,需要分情况处理 + const resultTaskIndex = step.tasks.findIndex((task) => task.type === 'result'); + if (resultTaskIndex >= 0) { + // 合并到已有结果 + step.tasks = step.tasks.map((task, index) => + index === resultTaskIndex ? { ...task, text: task.text + newChunk.content.text } : task, + ); + } else { + // 添加新结果 step.tasks.push({ - type: newchunk.state === 'agent_update' ? 'command' : 'result', - text: newchunk.content.text, + type: 'result', + text: newChunk.content.text, }); } - - // 设置step状态 - step.status = newchunk.state === 'agent_finish' ? 'finish' : 'pending'; - newExisting.content.steps[stepIndex] = step; } - return newExisting; + updated.content.steps[stepIndex] = step; + return updated; }); - - const update = (e: CustomEvent) => { - setMockMessage(e.detail); - }; - - chat.addEventListener('message_change', update); - return () => { - chat.removeEventListener('message_change', update); - }; }, []); return ( @@ -196,28 +219,22 @@ export default function ChatBotReact() { defaultMessages={mockData} messageProps={messageProps} senderProps={{ - defaultValue: '请帮我做一个家庭聚会任务规划', + defaultValue: '请帮我做一个5岁儿童生日聚会的规划', }} chatServiceConfig={chatServiceConfig} + onMessageChange={(e) => { + setMockMessage(e.detail); + }} > {mockMessage ?.map((data) => data.content.map((item) => { - switch (item.state) { - // 示例:智能体初始化 - case 'agent_init': - return ( -
- -
- ); - // 处理智能体更新状态 - case 'agent_update': - return ( -
-
{item.content.text}
-
- ); + if (item.type === 'agent') { + return ( +
+ +
+ ); } return null; }), diff --git a/packages/components/chatbot/_example/image.tsx b/packages/components/chatbot/_example/image.tsx index b965fd9de1..2d99b8ff32 100644 --- a/packages/components/chatbot/_example/image.tsx +++ b/packages/components/chatbot/_example/image.tsx @@ -10,6 +10,7 @@ import type { TdChatSenderParams, UploadFile, ChatRequestParams, + TdChatMessageConfig, } from 'tdesign-react'; import { Button, ChatBot, Dropdown, Space, Image, type TdChatbotApi, ImageViewer, Skeleton } from 'tdesign-react'; @@ -134,7 +135,7 @@ export default function chatSample() { const [mockMessage, setMockMessage] = React.useState(mockData); // 消息属性配置 - const messageProps = { + const messageProps: TdChatMessageConfig = { user: { variant: 'base', placement: 'right', diff --git a/packages/components/chatbot/_example/research.tsx b/packages/components/chatbot/_example/research.tsx index dd06a5bc1a..97cd8c6849 100644 --- a/packages/components/chatbot/_example/research.tsx +++ b/packages/components/chatbot/_example/research.tsx @@ -7,6 +7,7 @@ import type { TdAttachmentItem, UploadFile, ChatRequestParams, + TdChatMessageConfig, } from 'tdesign-react'; import { ChatBot, type TdChatbotApi } from 'tdesign-react'; @@ -19,7 +20,7 @@ const mockData: ChatMessagesData[] = [ { type: 'text', status: 'complete', - data: '欢迎使用TDesign文档阅读助手,请先上传你需要识别和理解的文件吧~', + data: '欢迎使用TDesign文档阅读助手,请先上传你需要识别和理解的文件,可以针对文档内容进行咨询~', }, ], }, @@ -30,7 +31,7 @@ export default function chatSample() { const [files, setFiles] = useState([]); // 消息属性配置 - const messageProps = { + const messageProps: TdChatMessageConfig = { user: { variant: 'base', placement: 'right', @@ -149,8 +150,9 @@ export default function chatSample() { defaultMessages={mockData} messageProps={messageProps} senderProps={{ + defaultValue: '根据所提供的材料总结一篇文章,需要符合公众号平台写作风格', placeholder: '上传你需要识别和理解的文件吧~', - actions: (preset) => preset, + actions: ['attachmentUploader', 'sendButton'], uploadProps: { multiple: true, }, diff --git a/packages/components/chatbot/chatbot.md b/packages/components/chatbot/chatbot.md index 1114a298ae..fab2a65c89 100644 --- a/packages/components/chatbot/chatbot.md +++ b/packages/components/chatbot/chatbot.md @@ -48,11 +48,10 @@ spline: navigation ### 图像生成 以下案例演示了使用Chatbot搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** - {{ image }} - -### 自主任务规划 +### 任务规划 +以下案例模拟了使用Chatbot搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**自定义消息内容合并策略**,同时演示了**自定义任务流程渲染** {{ agent }} diff --git a/server/chat/data/agent.js b/server/chat/data/agent.js index c428dc2665..cdf427dfb8 100644 --- a/server/chat/data/agent.js +++ b/server/chat/data/agent.js @@ -1,100 +1,266 @@ module.exports = [ + { type: 'text', msg: '为5岁' }, + { type: 'text', msg: '小朋友' }, + { type: 'text', msg: '准备' }, + { type: 'text', msg: '一场生日' }, + { type: 'text', msg: '派对,' }, + { type: 'text', msg: '我会' }, + { type: 'text', msg: '根据要求' }, + { type: 'text', msg: '准备' }, + { type: 'text', msg: '合适方案,' }, + { type: 'text', msg: '计划从' }, + { type: 'text', msg: '以下几个' }, + { type: 'text', msg: '步骤' }, + { type: 'text', msg: '进行准备:' }, { type: 'agent', - state: 'agent_init', - id: '111111', + state: 'init', + id: 'task1', content: { - text: '家庭聚会规划任务已分解为3个执行阶段', + text: '生日聚会规划任务已分解为3个执行阶段', steps: [ - { step: '① 餐饮方案', agent_id: 'a1', time: '2分钟' }, - { step: '② 设备调度', agent_id: 'a2', time: '3分钟' }, - { step: '③ 安全监测', agent_id: 'a3', time: '1分钟' }, + { step: '确定派对餐饮方案', agent_id: 'a1' }, + { step: '准备派对现场布置', agent_id: 'a2' }, + { step: '策划派对活动', agent_id: 'a3' }, ], }, }, { type: 'agent', - state: 'agent_update', - id: '222', + state: 'command', + id: 'task1-a1-c1', content: { agent_id: 'a1', - text: '开始生成餐饮方案:正在分析用户饮食偏好...', + text: '调用智能搜索工具,搜索儿童健康点心,儿童健康食谱', }, }, { type: 'agent', - state: 'agent_update', - id: '333', + state: 'command', + id: 'task1-a1-c2', content: { agent_id: 'a1', - text: '已筛选出3种高性价比菜单方案,正在进行营养匹配...', + text: '已筛选出3种高性价比菜单方案,开始进行营养匹配', }, }, { type: 'agent', - state: 'agent_result', - id: '444', + state: 'result', + id: 'task1-a1-result', content: { agent_id: 'a1', - text: '🍴 推荐餐饮方案:主菜是香草烤鸡(无麸质),准备耗时45分钟;饮品是智能调酒机方案B,酒精浓度12%', + text: '推荐餐饮方案: ', }, }, { type: 'agent', - state: 'agent_finish', - id: '44455', + state: 'result', + id: 'task1-a1-result', content: { agent_id: 'a1', + text: '主菜是香草烤鸡(无麸质),', }, }, { type: 'agent', - state: 'agent_update', - id: '555', + state: 'result', + id: 'task1-a1-result', + content: { + agent_id: 'a1', + text: '准备耗时45分钟;', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a1-result', + content: { + agent_id: 'a1', + text: '恐龙造型生日蛋糕,', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a1-result', + content: { + agent_id: 'a1', + text: '可食用果蔬汁调色的面团;', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a1-result', + content: { + agent_id: 'a1', + text: '水果蔬菜拼盘;', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a1-result', + content: { + agent_id: 'a1', + text: '饮品是鲜榨苹果汁,橙汁', + }, + }, + { + type: 'agent', + state: 'finish', + id: 'task1-a1-finish', + content: { + agent_id: 'a1', + }, + }, + { + type: 'agent', + state: 'command', + id: 'task1-a2-c1', + content: { + agent_id: 'a2', + text: '调用智能搜索工具,搜索儿童派对用品清单', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a2-result', + content: { + agent_id: 'a2', + text: '推荐现场布置方案:', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a2-result', + content: { + agent_id: 'a2', + text: '餐具(一次性纸盘、刀叉套装)', + }, + }, + { + type: 'agent', + state: 'result', + id: 'task1-a2-result', content: { agent_id: 'a2', - text: '设备调度中:已激活厨房智能设备...', + text: '、杯子、纸巾、一次性桌布,', }, }, { type: 'agent', - state: 'agent_result', - id: '666', + state: 'result', + id: 'task1-a2-result', content: { agent_id: 'a2', - text: '📱 设备调度方案:智能烤箱预热至180℃,倒计时09:15启动;环境调节至23℃,湿度55%', + text: '装饰气球、横幅、礼帽等,', }, }, { type: 'agent', - state: 'agent_finish', - id: '4445566', + state: 'result', + id: 'task1-a2-result', content: { agent_id: 'a2', + text: '根据来访人数,可以选择零售渠道,价格从1-15元不等', }, }, { type: 'agent', - state: 'agent_update', - id: '777', + state: 'result', + id: 'task1-a2-result', + content: { + agent_id: 'a2', + text: ',让孩子参与布置过程,增加互动性', + }, + }, + { + type: 'agent', + state: 'finish', + id: 'task1-a2-finish', + content: { + agent_id: 'a2', + }, + }, + { + type: 'agent', + state: 'command', + id: 'task1-a3-c1', + content: { + agent_id: 'a3', + text: '搜索儿童派对游戏,安全、有趣、简单', + }, + }, + { + type: 'agent', + state: 'command', + id: 'task1-a3-c2', + content: { + agent_id: 'a3', + text: '整理信息并进行合理性分析,安全性评估', + }, + }, + { + type: 'agent', + state: 'result', + id: '888', + content: { + agent_id: 'a3', + text: '派对总时长建议控制在1.5小时,', + }, + }, + { + type: 'agent', + state: 'result', + id: '888', + content: { + agent_id: 'a3', + text: '符合5岁儿童注意力持续时间,', + }, + }, + { + type: 'agent', + state: 'result', + id: '888', + content: { + agent_id: 'a3', + text: '每位小朋友到达时可以在拍照区留影,', + }, + }, + { + type: 'agent', + state: 'result', + id: '888', + content: { + agent_id: 'a3', + text: '可设置一个签到板,', + }, + }, + { + type: 'agent', + state: 'result', + id: '888', content: { agent_id: 'a3', - text: '安全巡检完成:未发现燃气泄漏风险', + text: '推荐活动:', }, }, { type: 'agent', - state: 'agent_result', + state: 'result', id: '888', content: { agent_id: 'a3', - text: '所有智能体已完成协作', + text: '尾巴追逐赛,彩泥制作,套圈,抽盲盒', }, }, { type: 'agent', - state: 'agent_finish', - id: '444556677', + state: 'finish', + id: 'task1-a3-finish', content: { agent_id: 'a3', }, diff --git a/server/chat/data/code.js b/server/chat/data/code.js index 0725d6d945..0031d8a57a 100644 --- a/server/chat/data/code.js +++ b/server/chat/data/code.js @@ -496,11 +496,10 @@ module.exports = [ data: { id: Date.now(), enName: 'tdesign-login-form.jsx', - cnName: 'TDesign登录表单示例,代码生成完成,开始自动化测试...', + cnName: 'TDesign登录表单示例,开始自动化测试...', version: 'v1', }, }, - { type: 'text', msg: '这个', paragraph: 'next' }, { type: 'text', msg: '版本' }, From 13c7012368300cac55e19ab549da3dff4caf23fe Mon Sep 17 00:00:00 2001 From: carolin913 Date: Fri, 16 May 2025 19:03:24 +0800 Subject: [PATCH 059/228] feat(chatthinking): demo refine --- packages/components/chat-thinking/_example/base.tsx | 1 - packages/components/chat-thinking/chat-thinking.md | 6 ++++++ packages/components/chatbot/_example/basic.tsx | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/components/chat-thinking/_example/base.tsx b/packages/components/chat-thinking/_example/base.tsx index ff49e211bf..5072633275 100644 --- a/packages/components/chat-thinking/_example/base.tsx +++ b/packages/components/chat-thinking/_example/base.tsx @@ -45,7 +45,6 @@ export default function ThinkContentDemo() { text: displayText, }} status={status} - maxHeight={100} /> ); } diff --git a/packages/components/chat-thinking/chat-thinking.md b/packages/components/chat-thinking/chat-thinking.md index 499a5779ef..1bb22f304b 100644 --- a/packages/components/chat-thinking/chat-thinking.md +++ b/packages/components/chat-thinking/chat-thinking.md @@ -11,6 +11,12 @@ spline: navigation {{ base }} +## 样式设置 +支持通过layout来设置思考过程的布局方式,支持通过animation来设置思考过程的动画效果 + +{{ style }} + + ## API ### Chatbot Props diff --git a/packages/components/chatbot/_example/basic.tsx b/packages/components/chatbot/_example/basic.tsx index 74df2efb4e..4385e414b0 100644 --- a/packages/components/chatbot/_example/basic.tsx +++ b/packages/components/chatbot/_example/basic.tsx @@ -91,6 +91,7 @@ export default function chatSample() { chatContentProps: { thinking: { maxHeight: 100, // 思考框最大高度,超过会自动滚动 + layout: 'border', // 思考内容样式,border|block collapsed: thinking?.status === 'complete', // 是否折叠,这里设置内容输出完成后折叠 }, }, From 4769efbe6f00b5329f14b38409b5ea7ad98cee03 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Fri, 16 May 2025 19:04:45 +0800 Subject: [PATCH 060/228] feat(chatthining): style demo n --- .../chat-thinking/_example/base.tsx | 1 + .../chat-thinking/_example/style.tsx | 97 +++++++++++++++++++ .../components/chat-thinking/chat-thinking.md | 2 +- 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/components/chat-thinking/_example/style.tsx diff --git a/packages/components/chat-thinking/_example/base.tsx b/packages/components/chat-thinking/_example/base.tsx index 5072633275..ff49e211bf 100644 --- a/packages/components/chat-thinking/_example/base.tsx +++ b/packages/components/chat-thinking/_example/base.tsx @@ -45,6 +45,7 @@ export default function ThinkContentDemo() { text: displayText, }} status={status} + maxHeight={100} /> ); } diff --git a/packages/components/chat-thinking/_example/style.tsx b/packages/components/chat-thinking/_example/style.tsx new file mode 100644 index 0000000000..2de373b866 --- /dev/null +++ b/packages/components/chat-thinking/_example/style.tsx @@ -0,0 +1,97 @@ +import { TdChatThinkContentProps, type MessageStatus } from '@tencent/tdesign-chatbot'; +import React, { useState, useEffect, useRef } from 'react'; +import { ChatThinking, Radio, RadioOption } from 'tdesign-react'; +import Space from '../../space/Space'; + +const fullText = + '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。'; + +const objOptions: RadioOption[] = [ + { + value: 'border', + label: 'border', + }, + { + value: 'block', + label: 'block', + }, +]; + +export default function ThinkContentDemo() { + const [displayText, setDisplayText] = useState(''); + const [status, setStatus] = useState('pending'); + const [title, setTitle] = useState('正在思考中...'); + const [layout, setLayout] = useState('block'); + const [animation, setAnimation] = useState('circle'); + const timerRef = useRef>(null); + const currentIndex = useRef(0); + const startTimeRef = useRef(Date.now()); + + useEffect(() => { + // 每次layout变化时重置状态 + resetTypingEffect(); + // 模拟打字效果 + const typeEffect = () => { + if (currentIndex.current < fullText.length) { + const char = fullText[currentIndex.current]; + currentIndex.current += 1; + setDisplayText((prev) => prev + char); + timerRef.current = setTimeout(typeEffect, 50); + setStatus('streaming'); + } else { + // 计算耗时并更新状态 + const costTime = parseInt(((Date.now() - startTimeRef.current) / 1000).toString(), 10); + setTitle(`已完成思考(耗时${costTime}秒)`); + setStatus('complete'); + } + }; + + startTimeRef.current = Date.now(); + timerRef.current = setTimeout(typeEffect, 500); + + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [layout, animation]); + + // 重置打字效果相关状态 + const resetTypingEffect = () => { + setDisplayText(''); + setStatus('pending'); + setTitle('正在思考中...'); + currentIndex.current = 0; + if (timerRef.current) clearTimeout(timerRef.current); + }; + + return ( + + + +
layout:
+ setLayout(val)}> + border + block + +
+ +
animation:
+ setAnimation(val)}> + {/* skeleton */} + moving + gradient + circle + +
+
+ +
+ ); +} diff --git a/packages/components/chat-thinking/chat-thinking.md b/packages/components/chat-thinking/chat-thinking.md index 1bb22f304b..06ac4f4ca5 100644 --- a/packages/components/chat-thinking/chat-thinking.md +++ b/packages/components/chat-thinking/chat-thinking.md @@ -7,7 +7,7 @@ spline: navigation --- ## 基础用法 - +支持通过maxHeight来设置展示内容的最大高度,超出会自动滚动 {{ base }} From 35c357a8a8e20d04d0e8573cd558eb0944e4d865 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 19 May 2025 11:46:58 +0800 Subject: [PATCH 061/228] feat(chatbot): doc demo --- .../chat-thinking/_example/base.tsx | 8 + .../components/chat-thinking/chat-thinking.md | 9 +- .../_example/{research.tsx => docs.tsx} | 8 +- packages/components/chatbot/chatbot.md | 6 +- server/chat/data/docs.js | 283 ++++++++++++++++++ server/chat/ssemock.js | 7 +- 6 files changed, 311 insertions(+), 10 deletions(-) rename packages/components/chatbot/_example/{research.tsx => docs.tsx} (92%) create mode 100644 server/chat/data/docs.js diff --git a/packages/components/chat-thinking/_example/base.tsx b/packages/components/chat-thinking/_example/base.tsx index ff49e211bf..db5707037d 100644 --- a/packages/components/chat-thinking/_example/base.tsx +++ b/packages/components/chat-thinking/_example/base.tsx @@ -9,6 +9,7 @@ export default function ThinkContentDemo() { const [displayText, setDisplayText] = useState(''); const [status, setStatus] = useState('pending'); const [title, setTitle] = useState('正在思考中...'); + const [collapsed, setCollapsed] = useState(false); const timerRef = useRef>(null); const currentIndex = useRef(0); const startTimeRef = useRef(Date.now()); @@ -38,6 +39,12 @@ export default function ThinkContentDemo() { }; }, []); + useEffect(() => { + if (status === 'complete') { + setCollapsed(true); // 内容结束输出后收起面板 + } + }, [status]); + return ( ); } diff --git a/packages/components/chat-thinking/chat-thinking.md b/packages/components/chat-thinking/chat-thinking.md index 06ac4f4ca5..70eb37a5d6 100644 --- a/packages/components/chat-thinking/chat-thinking.md +++ b/packages/components/chat-thinking/chat-thinking.md @@ -7,12 +7,17 @@ spline: navigation --- ## 基础用法 -支持通过maxHeight来设置展示内容的最大高度,超出会自动滚动 +支持通过`maxHeight`来设置展示内容的最大高度,超出会自动滚动; + +支持通过`collapsed`来控制面板是否折叠,示例中展示了当内容输出结束时自动收起的效果 + {{ base }} ## 样式设置 -支持通过layout来设置思考过程的布局方式,支持通过animation来设置思考过程的动画效果 +支持通过`layout`来设置思考过程的布局方式 + +支持通过`animation`来设置思考过程的动画效果 {{ style }} diff --git a/packages/components/chatbot/_example/research.tsx b/packages/components/chatbot/_example/docs.tsx similarity index 92% rename from packages/components/chatbot/_example/research.tsx rename to packages/components/chatbot/_example/docs.tsx index 97cd8c6849..37907072b6 100644 --- a/packages/components/chatbot/_example/research.tsx +++ b/packages/components/chatbot/_example/docs.tsx @@ -20,7 +20,7 @@ const mockData: ChatMessagesData[] = [ { type: 'text', status: 'complete', - data: '欢迎使用TDesign文档阅读助手,请先上传你需要识别和理解的文件,可以针对文档内容进行咨询~', + data: '欢迎使用TDesign文案写作助手,可以先上传你需要参考的文件,输入你要撰写的主题~', }, ], }, @@ -39,7 +39,7 @@ export default function chatSample() { }, assistant: { placement: 'left', - actions: ['good', 'bad'], + actions: ['copy', 'good', 'bad'], handleActions: { // 处理消息操作回调 good: async ({ message, active }) => { @@ -150,8 +150,8 @@ export default function chatSample() { defaultMessages={mockData} messageProps={messageProps} senderProps={{ - defaultValue: '根据所提供的材料总结一篇文章,需要符合公众号平台写作风格', - placeholder: '上传你需要识别和理解的文件吧~', + defaultValue: '根据所提供的材料总结一篇文章,推荐春天户外郊游打卡目的地,需要符合小红书平台写作风格', + placeholder: '输入你要撰写的主题,支持上传附件', actions: ['attachmentUploader', 'sendButton'], uploadProps: { multiple: true, diff --git a/packages/components/chatbot/chatbot.md b/packages/components/chatbot/chatbot.md index fab2a65c89..4c4b9b7b63 100644 --- a/packages/components/chatbot/chatbot.md +++ b/packages/components/chatbot/chatbot.md @@ -42,9 +42,9 @@ spline: navigation 通过使用tdesign开发登录框组件的案例,演示了使用Chatbot搭建简单的代码助手场景,该示例你可以了解到如何按需开启**markdown渲染代码块**,如何**自定义实现代码预览** {{ code }} -### 文档理解 -以下案例演示了使用Chatbot搭建简单的文件理解应用,通过该示例你可以了解到如何**使用附件区域**,同时演示了**附件类型的内容渲染** -{{ research }} +### 文案助手 +以下案例演示了使用Chatbot搭建简单的文案写作助手应用,通过该示例你可以了解到如何**使用附件区域**,同时演示了**附件类型的内容渲染** +{{ docs }} ### 图像生成 以下案例演示了使用Chatbot搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** diff --git a/server/chat/data/docs.js b/server/chat/data/docs.js new file mode 100644 index 0000000000..fdefb0009e --- /dev/null +++ b/server/chat/data/docs.js @@ -0,0 +1,283 @@ +module.exports = [ + { type: 'text', msg: '🌼' }, + + { type: 'text', msg: '宝' }, + + { type: 'text', msg: '子' }, + + { type: 'text', msg: '们' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '春天' }, + + { type: 'text', msg: '来' }, + + { type: 'text', msg: '啦' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '这些' }, + + { type: 'text', msg: '户外' }, + + { type: 'text', msg: '郊' }, + + { type: 'text', msg: '游' }, + + { type: 'text', msg: '打卡' }, + + { type: 'text', msg: '地' }, + + { type: 'text', msg: '你必须' }, + + { type: 'text', msg: '知道' }, + + { type: 'text', msg: '👏' }, + + { type: 'text', msg: '\n\n' }, + + { type: 'text', msg: '🌟' }, + + { type: 'text', msg: '郊' }, + + { type: 'text', msg: '野' }, + + { type: 'text', msg: '公园' }, + + { type: 'text', msg: '\n' }, + + { type: 'text', msg: '这里' }, + + { type: 'text', msg: '有大' }, + + { type: 'text', msg: '片的' }, + + { type: 'text', msg: '草地' }, + + { type: 'text', msg: '和' }, + + { type: 'text', msg: '各种' }, + + { type: 'text', msg: '花卉' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '随便' }, + + { type: 'text', msg: '一' }, + + { type: 'text', msg: '拍' }, + + { type: 'text', msg: '都是' }, + + { type: 'text', msg: '大片' }, + + { type: 'text', msg: '既' }, + + { type: 'text', msg: '视' }, + + { type: 'text', msg: '感' }, + + { type: 'text', msg: '📷' }, + + { type: 'text', msg: '。' }, + + { type: 'text', msg: '还能' }, + + { type: 'text', msg: '放' }, + + { type: 'text', msg: '风筝' }, + + { type: 'text', msg: '、' }, + + { type: 'text', msg: '野' }, + + { type: 'text', msg: '餐' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '享受' }, + + { type: 'text', msg: '惬' }, + + { type: 'text', msg: '意的' }, + + { type: 'text', msg: '春' }, + + { type: 'text', msg: '日' }, + + { type: 'text', msg: '时光' }, + + { type: 'text', msg: '。\n\n' }, + + { type: 'text', msg: '🌳' }, + + { type: 'text', msg: '植物' }, + + { type: 'text', msg: '园' }, + + { type: 'text', msg: '\n' }, + + { type: 'text', msg: '各种' }, + + { type: 'text', msg: '珍' }, + + { type: 'text', msg: '稀' }, + + { type: 'text', msg: '植物' }, + + { type: 'text', msg: '汇聚' }, + + { type: 'text', msg: '于此' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '仿佛' }, + + { type: 'text', msg: '置身' }, + + { type: 'text', msg: '于' }, + + { type: 'text', msg: '绿色的' }, + + { type: 'text', msg: '海洋' }, + + { type: 'text', msg: '。' }, + + { type: 'text', msg: '漫步' }, + + { type: 'text', msg: '其中' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '感受' }, + + { type: 'text', msg: '大' }, + + { type: 'text', msg: '自然的' }, + + { type: 'text', msg: '神奇' }, + + { type: 'text', msg: '与' }, + + { type: 'text', msg: '美丽' }, + + { type: 'text', msg: '。\n\n' }, + + { type: 'text', msg: '💧' }, + + { type: 'text', msg: '湖' }, + + { type: 'text', msg: '边' }, + + { type: 'text', msg: '湿地' }, + + { type: 'text', msg: '\n' }, + + { type: 'text', msg: '湖' }, + + { type: 'text', msg: '水' }, + + { type: 'text', msg: '清澈' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '周围' }, + + { type: 'text', msg: '生态环境' }, + + { type: 'text', msg: '优越' }, + + { type: 'text', msg: '。' }, + + { type: 'text', msg: '能看到' }, + + { type: 'text', msg: '很多' }, + + { type: 'text', msg: '候' }, + + { type: 'text', msg: '鸟' }, + + { type: 'text', msg: '和水' }, + + { type: 'text', msg: '生' }, + + { type: 'text', msg: '植物' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '是' }, + + { type: 'text', msg: '亲近' }, + + { type: 'text', msg: '自然' }, + + { type: 'text', msg: '的好' }, + + { type: 'text', msg: '去' }, + + { type: 'text', msg: '处' }, + + { type: 'text', msg: '。\n\n' }, + + { type: 'text', msg: '宝' }, + + { type: 'text', msg: '子' }, + + { type: 'text', msg: '们' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '赶紧' }, + + { type: 'text', msg: '收拾' }, + + { type: 'text', msg: '行' }, + + { type: 'text', msg: '囊' }, + + { type: 'text', msg: ',' }, + + { type: 'text', msg: '去' }, + + { type: 'text', msg: '这些' }, + + { type: 'text', msg: '地方' }, + + { type: 'text', msg: '打卡' }, + + { type: 'text', msg: '吧' }, + + { type: 'text', msg: '😜' }, + + { type: 'text', msg: '。\n\n' }, + + { type: 'text', msg: '#' }, + + { type: 'text', msg: '春天' }, + + { type: 'text', msg: '郊' }, + + { type: 'text', msg: '游' }, + + { type: 'text', msg: ' #' }, + + { type: 'text', msg: '打卡' }, + + { type: 'text', msg: '目的地' }, + + { type: 'text', msg: ' #' }, + + { type: 'text', msg: '户外' }, + + { type: 'text', msg: '之旅' }, + + { type: 'text', msg: ' #' }, + + { type: 'text', msg: '春' }, + + { type: 'text', msg: '日' }, + + { type: 'text', msg: '美景' }, +]; diff --git a/server/chat/ssemock.js b/server/chat/ssemock.js index 725a7d359d..b50b984a8a 100644 --- a/server/chat/ssemock.js +++ b/server/chat/ssemock.js @@ -5,6 +5,7 @@ const chunksChart = require('./data/chart'); const chunksCode = require('./data/code'); const chunksImage = require('./data/image'); const agentChunks = require('./data/agent'); +const chunksDoc = require('./data/docs'); const app = express(); app.use(cors()); @@ -62,11 +63,15 @@ app.post('/sse/normal', (req, res) => { setSSEHeaders(res); let mockdata = chunks; - const { think = false, search = false, chart = false, code = false, image = false } = req.body; + const { think = false, search = false, chart = false, code = false, image = false, docs = false } = req.body; if (chart) { mockdata = chunksChart; } + if (docs) { + mockdata = chunksDoc; + } + if (code) { mockdata = chunksCode; } From f17b82322cf5171e76df2e7d2b40d86481389e48 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 19 May 2025 12:10:03 +0800 Subject: [PATCH 062/228] feat(chatbot): depes chat v55 --- package.json | 2 +- packages/components/chatbot/chatbot.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f08736dade..6893e469c5 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,6 @@ "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0", - "@tencent/tdesign-chatbot": "1.0.0-beta.48" + "@tencent/tdesign-chatbot": "1.0.0-beta.55" } } diff --git a/packages/components/chatbot/chatbot.md b/packages/components/chatbot/chatbot.md index 4c4b9b7b63..8d462d8174 100644 --- a/packages/components/chatbot/chatbot.md +++ b/packages/components/chatbot/chatbot.md @@ -43,7 +43,7 @@ spline: navigation {{ code }} ### 文案助手 -以下案例演示了使用Chatbot搭建简单的文案写作助手应用,通过该示例你可以了解到如何**使用附件区域**,同时演示了**附件类型的内容渲染** +以下案例演示了使用Chatbot搭建简单的文案写作助手应用,通过该示例你可以了解到如何**发送附件**,同时演示了**附件类型的内容渲染** {{ docs }} ### 图像生成 From 01a4db9e9e6c0df41065a0af72848e1e45cac3ee Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 19 May 2025 19:14:09 +0800 Subject: [PATCH 063/228] feat(chatsender): attachment demo --- .../chat-sender/_example/attachment.tsx | 12 ++--- .../chat-sender/_example/custom.tsx | 49 +++++++++++++++---- .../components/chat-sender/chat-sender.md | 1 + packages/components/chatbot/_example/docs.tsx | 2 +- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/components/chat-sender/_example/attachment.tsx b/packages/components/chat-sender/_example/attachment.tsx index 43e1792303..0e0890e1fc 100644 --- a/packages/components/chat-sender/_example/attachment.tsx +++ b/packages/components/chat-sender/_example/attachment.tsx @@ -16,6 +16,7 @@ const ChatSenderExample = () => { { name: 'image-file.png', size: 333333, + url: 'https://tdesign.gtimg.com/site/avatar.jpg', }, ]); @@ -30,6 +31,7 @@ const ChatSenderExample = () => { console.log('提交', { value: inputValue }); setInputValue(''); setLoading(true); + setFiles([]); }; // 停止处理 @@ -47,13 +49,13 @@ const ChatSenderExample = () => { // 添加新文件并模拟上传进度 let newFile = { ...e.detail[0], + size: e.detail[0].size, name: e.detail[0].name, status: 'progress' as UploadFile['status'], description: '上传中', }; setFiles((prev) => [newFile, ...prev]); - setTimeout(() => { setFiles((prevState) => prevState.map((file) => @@ -62,7 +64,7 @@ const ChatSenderExample = () => { ...file, url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'success', - description: '上传成功', + description: `${Math.floor(newFile.size / 1024)}KB`, } : file, ), @@ -75,11 +77,7 @@ const ChatSenderExample = () => { value={inputValue} placeholder="请输入内容" loading={loading} - actions={(preset) => preset} - uploadProps={{ - multiple: true, - accept: 'image/*', - }} + actions={['attachment', 'send']} attachmentsProps={{ items: files, overflow: 'scrollX', diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/components/chat-sender/_example/custom.tsx index 31b8836112..2ef9e9659d 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/components/chat-sender/_example/custom.tsx @@ -1,6 +1,7 @@ +import { TdAttachmentItem } from '@tencent/tdesign-chatbot'; import React, { useRef, useState, useEffect } from 'react'; import { EnterIcon, InternetIcon, AttachIcon, CloseIcon, ArrowUpIcon, StopIcon } from 'tdesign-icons-react'; -import { ChatSender, Space, Button, Tag, Dropdown, Tooltip } from 'tdesign-react'; +import { ChatSender, Space, Button, Tag, Dropdown, Tooltip, UploadFile } from 'tdesign-react'; const options = [ { @@ -24,6 +25,7 @@ const ChatSenderExample = () => { const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); const senderRef = useRef(null); + const [files, setFiles] = useState([]); const [scene, setScene] = useState(1); const [showRef, setShowRef] = useState(true); const [activeR1, setR1Active] = useState(false); @@ -32,9 +34,6 @@ const ChatSenderExample = () => { // 使用变量生成自定义组件样式 const generateScopedStyles = () => ` - .t-popup__content { - padding: 0; - } .${styleId.current} { --td-text-color-placeholder: #DFE2E7; --td-bg-color-secondarycontainer: #fff; @@ -70,6 +69,7 @@ const ChatSenderExample = () => { console.log('提交', { value: inputValue }); setInputValue(''); setLoading(true); + setFiles([]); }; // 停止处理 @@ -84,7 +84,29 @@ const ChatSenderExample = () => { }; const onFileSelect = (e: CustomEvent) => { - console.log('===selectfile', e.detail); + // 添加新文件并模拟上传进度 + let newFile = { + ...e.detail[0], + name: e.detail[0].name, + status: 'progress' as UploadFile['status'], + description: '上传中', + }; + + setFiles((prev) => [newFile, ...prev]); + + setTimeout(() => { + setFiles((prevState) => + prevState.map((file) => + file.name === newFile.name + ? { + ...file, + url: 'https://tdesign.gtimg.com/site/avatar.jpg', + status: 'success', + } + : file, + ), + ); + }, 1000); }; const switchScene = (data) => { @@ -95,6 +117,10 @@ const ChatSenderExample = () => { setShowRef(false); }; + const onAttachmentsRemove = (e: CustomEvent) => { + setFiles(e.detail); + }; + return ( { onSend={handleSend} onStop={handleStop} onFileSelect={onFileSelect} + onFileRemove={onAttachmentsRemove} + uploadProps={{ + accept: 'image/*', + }} + attachmentsProps={{ + items: files, + }} > {/* 自定义输入框上方区域,可用来引用内容或提示场景 */} {showRef && ( @@ -134,11 +167,7 @@ const ChatSenderExample = () => { {/* 自定义输入框底部区域slot,可以增加模型选项 */}
- +
{/* 自定义输入框左侧区域slot,可以用来触发工具场景切换 */}
- + {options.filter((item) => item.value === scene)[0].content} diff --git a/packages/components/chat-sender/chat-sender.md b/packages/components/chat-sender/chat-sender.md index 125a459b3c..0d492c050b 100644 --- a/packages/components/chat-sender/chat-sender.md +++ b/packages/components/chat-sender/chat-sender.md @@ -8,15 +8,21 @@ spline: navigation ## 基础用法 - +受控进行输入/发送等状态管理 {{ base }} -## 附件输入 +## 附件输入 +支持选择附件及展示附件列表,受控进行文件数据管理,示例中模拟了文件上传流程 {{ attachment }} ## 自定义 +通过植入具名插槽来实现输入框的自定义,内置支持的扩展位置包括: + +输入框上方区域`header`,输入框内头部区域`inner-header`,可输入区域前置部分`prefix`,输入框底部左侧区域`footer-left`,输入框底部操作区域`actions` + +同时示例中演示了通过`CSS变量覆盖`实现样式定制 {{ custom }} From 2d5118bf8c9bc3aad6fb53bca9da8bbecc895586 Mon Sep 17 00:00:00 2001 From: Uyarn Date: Mon, 19 May 2025 00:02:05 +0800 Subject: [PATCH 065/228] feat: react aigc workspaces framework --- .vscode/settings.json | 3 +- package.json | 6 + packages/components/index.ts | 14 +- .../chat}/_util/reactify.tsx | 0 .../chat}/chatbot/_example/agent.tsx | 53 +-- .../chat}/chatbot/_example/basic.tsx | 8 +- .../chat}/chatbot/_example/code.tsx | 8 +- .../chatbot/_example/components/login.tsx | 0 .../chat}/chatbot/_example/custom.tsx | 0 .../chat}/chatbot/_example/hookComponent.tsx | 0 .../chat}/chatbot/_example/image.tsx | 0 .../chat}/chatbot/_example/index.css | 0 .../chat}/chatbot/_example/research.tsx | 0 .../chat}/chatbot/_example/searchContent.tsx | 0 .../chatbot/_example/suggestionContent.tsx | 0 .../chat}/chatbot/chatbot.en-US.md | 0 .../chat}/chatbot/chatbot.md | 12 - .../chat}/chatbot/index.ts | 0 .../chat}/chatbot/useChat.ts | 0 packages/pro-components/chat/index.ts | 1 + packages/pro-components/chat/package.json | 7 + packages/tdesign-react-aigc/package.json | 77 +++ packages/tdesign-react-aigc/site/.gitignore | 6 + packages/tdesign-react-aigc/site/README.md | 4 + .../site/babel.config.demo.js | 3 + .../tdesign-react-aigc/site/babel.config.js | 4 + .../site/docs/getting-started.md | 34 ++ packages/tdesign-react-aigc/site/docs/sse.md | 213 +++++++++ packages/tdesign-react-aigc/site/index.html | 31 ++ packages/tdesign-react-aigc/site/package.json | 43 ++ .../tdesign-react-aigc/site/playground.html | 15 + .../site/plugin-tdoc/demo.js | 54 +++ .../site/plugin-tdoc/index.js | 25 + .../site/plugin-tdoc/md-to-react.js | 265 +++++++++++ .../site/plugin-tdoc/transforms.js | 89 ++++ .../site/public/apple-touch-icon.png | Bin 0 -> 16077 bytes .../site/public/favicon.ico | Bin 0 -> 16958 bytes .../tdesign-react-aigc/site/public/logo.svg | 41 ++ .../site/public/pwa-192x192.png | Bin 0 -> 17924 bytes .../site/public/pwa-512x512.png | Bin 0 -> 103459 bytes packages/tdesign-react-aigc/site/public/sw.js | 8 + packages/tdesign-react-aigc/site/pwaConfig.js | 25 + .../tdesign-react-aigc/site/site.config.mjs | 75 +++ packages/tdesign-react-aigc/site/src/App.jsx | 154 ++++++ .../site/src/components/BaseUsage.jsx | 84 ++++ .../site/src/components/Demo.jsx | 43 ++ .../site/src/components/Playground.jsx | 85 ++++ .../src/components/codesandbox/content.js | 111 +++++ .../site/src/components/codesandbox/index.jsx | 113 +++++ .../site/src/components/stackblitz/content.js | 134 ++++++ .../site/src/components/stackblitz/index.jsx | 68 +++ packages/tdesign-react-aigc/site/src/main.jsx | 27 ++ packages/tdesign-react-aigc/site/src/pwa.js | 10 + .../site/src/styles/Codesandbox.less | 24 + packages/tdesign-react-aigc/site/src/sw.js | 8 + packages/tdesign-react-aigc/site/src/utils.js | 24 + .../tdesign-react-aigc/site/test-coverage.js | 446 ++++++++++++++++++ .../tdesign-react-aigc/site/vite.config.js | 56 +++ packages/tdesign-react/site/site.config.mjs | 80 ---- pnpm-workspace.yaml | 3 +- 60 files changed, 2453 insertions(+), 141 deletions(-) rename packages/{components => pro-components/chat}/_util/reactify.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/agent.tsx (87%) rename packages/{components => pro-components/chat}/chatbot/_example/basic.tsx (98%) rename packages/{components => pro-components/chat}/chatbot/_example/code.tsx (97%) rename packages/{components => pro-components/chat}/chatbot/_example/components/login.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/custom.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/hookComponent.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/image.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/index.css (100%) rename packages/{components => pro-components/chat}/chatbot/_example/research.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/searchContent.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/_example/suggestionContent.tsx (100%) rename packages/{components => pro-components/chat}/chatbot/chatbot.en-US.md (100%) rename packages/{components => pro-components/chat}/chatbot/chatbot.md (88%) rename packages/{components => pro-components/chat}/chatbot/index.ts (100%) rename packages/{components => pro-components/chat}/chatbot/useChat.ts (100%) create mode 100644 packages/pro-components/chat/index.ts create mode 100644 packages/pro-components/chat/package.json create mode 100644 packages/tdesign-react-aigc/package.json create mode 100644 packages/tdesign-react-aigc/site/.gitignore create mode 100644 packages/tdesign-react-aigc/site/README.md create mode 100644 packages/tdesign-react-aigc/site/babel.config.demo.js create mode 100644 packages/tdesign-react-aigc/site/babel.config.js create mode 100644 packages/tdesign-react-aigc/site/docs/getting-started.md create mode 100644 packages/tdesign-react-aigc/site/docs/sse.md create mode 100644 packages/tdesign-react-aigc/site/index.html create mode 100644 packages/tdesign-react-aigc/site/package.json create mode 100644 packages/tdesign-react-aigc/site/playground.html create mode 100644 packages/tdesign-react-aigc/site/plugin-tdoc/demo.js create mode 100644 packages/tdesign-react-aigc/site/plugin-tdoc/index.js create mode 100644 packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js create mode 100644 packages/tdesign-react-aigc/site/plugin-tdoc/transforms.js create mode 100644 packages/tdesign-react-aigc/site/public/apple-touch-icon.png create mode 100644 packages/tdesign-react-aigc/site/public/favicon.ico create mode 100644 packages/tdesign-react-aigc/site/public/logo.svg create mode 100644 packages/tdesign-react-aigc/site/public/pwa-192x192.png create mode 100644 packages/tdesign-react-aigc/site/public/pwa-512x512.png create mode 100644 packages/tdesign-react-aigc/site/public/sw.js create mode 100644 packages/tdesign-react-aigc/site/pwaConfig.js create mode 100644 packages/tdesign-react-aigc/site/site.config.mjs create mode 100644 packages/tdesign-react-aigc/site/src/App.jsx create mode 100644 packages/tdesign-react-aigc/site/src/components/BaseUsage.jsx create mode 100644 packages/tdesign-react-aigc/site/src/components/Demo.jsx create mode 100644 packages/tdesign-react-aigc/site/src/components/Playground.jsx create mode 100644 packages/tdesign-react-aigc/site/src/components/codesandbox/content.js create mode 100644 packages/tdesign-react-aigc/site/src/components/codesandbox/index.jsx create mode 100644 packages/tdesign-react-aigc/site/src/components/stackblitz/content.js create mode 100644 packages/tdesign-react-aigc/site/src/components/stackblitz/index.jsx create mode 100644 packages/tdesign-react-aigc/site/src/main.jsx create mode 100644 packages/tdesign-react-aigc/site/src/pwa.js create mode 100644 packages/tdesign-react-aigc/site/src/styles/Codesandbox.less create mode 100644 packages/tdesign-react-aigc/site/src/sw.js create mode 100644 packages/tdesign-react-aigc/site/src/utils.js create mode 100644 packages/tdesign-react-aigc/site/test-coverage.js create mode 100644 packages/tdesign-react-aigc/site/vite.config.js diff --git a/.vscode/settings.json b/.vscode/settings.json index dd091a7fc4..574be515b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,7 +36,8 @@ "Cascader", "Popconfirm", "Swiper", - "tdesign" + "tdesign", + "aigc" ], "explorer.fileNesting.enabled": true, "explorer.fileNesting.expand": false, diff --git a/package.json b/package.json index f08736dade..54fa8aea8d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "site": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react/site build", "site:intranet": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react/site intranet", "site:preview": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react/site preview", + "dev:aigc": "pnpm -C packages/tdesign-react-aigc/site dev", + "site:aigc": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react-aigc/site build", + "site:aigc-intranet": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react-aigc/site intranet", + "site:aigc-preview": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react-aigc/site preview", "lint": "pnpm run lint:tsc && eslint --ext .ts,.tsx ./ --max-warnings 0", "lint:fix": "eslint --ext .ts,.tsx ./packages/components --ignore-pattern packages/components/__tests__ --max-warnings 0 --fix", "lint:tsc": "tsc -p ./tsconfig.dev.json ", @@ -163,7 +167,9 @@ "@tdesign/common-js": "workspace:^", "@tdesign/common-style": "workspace:^", "@tdesign/components": "workspace:^", + "@tdesign/pro-components-chat": "workspace:^", "@tdesign/react-site": "workspace:^", + "@tdesign-react/aigc": "workspace:^", "@types/sortablejs": "^1.10.7", "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.1.3", diff --git a/packages/components/index.ts b/packages/components/index.ts index 9df4f4813d..d5f9fcd106 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -1,4 +1,4 @@ -// export * from './layout'; +export * from './layout'; export * from './grid'; export * from './loading'; export * from './popup'; @@ -68,15 +68,3 @@ export * from './statistic'; export * from './descriptions'; export * from './empty'; export * from './typography'; -export * from './chat-loading'; -export * from './chat-thinking'; -export * from './chat-sender'; -export * from './chat-message'; -export * from './chat-actionbar'; -export * from './chat-attachment'; -export * from './chat-filecard'; -export * from './chat-markdown'; -export * from './chatbot'; -export * from './chat-loading'; -export * from './chat-thinking'; -export * from './chat-sender'; diff --git a/packages/components/_util/reactify.tsx b/packages/pro-components/chat/_util/reactify.tsx similarity index 100% rename from packages/components/_util/reactify.tsx rename to packages/pro-components/chat/_util/reactify.tsx diff --git a/packages/components/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx similarity index 87% rename from packages/components/chatbot/_example/agent.tsx rename to packages/pro-components/chat/chatbot/_example/agent.tsx index 52544ec3d1..061bc5181d 100644 --- a/packages/components/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef } from 'react'; import type { - SSEChunkData, TdChatMessageConfig, AIMessageContent, RequestParams, @@ -8,11 +7,13 @@ import type { BaseContent, ChatMessagesData, TdChatCustomRenderConfig, -} from 'tdesign-react'; -import { ChatBot, Timeline } from 'tdesign-react'; +} from '@tdesign-react/aigc'; +import { Timeline } from 'tdesign-react'; import { CheckCircleFilledIcon } from 'tdesign-icons-react'; +import { ChatBot } from '@tdesign-react/aigc'; + import './index.css'; // 自定义渲染-注册插槽规则 @@ -22,30 +23,28 @@ const customRenderConfig: TdChatCustomRenderConfig = { }), }; -const AgentTimeline = ({ steps }) => { - return ( -
- - {steps.map((step) => ( - } - > -
-
{step.step}
- {step?.tasks?.map((task, taskIndex) => ( -
-
{task.text}
-
- ))} -
-
- ))} -
-
- ); -}; +const AgentTimeline = ({ steps }) => ( +
+ + {steps.map((step) => ( + } + > +
+
{step.step}
+ {step?.tasks?.map((task, taskIndex) => ( +
+
{task.text}
+
+ ))} +
+
+ ))} +
+
+); // 扩展自定义消息体类型 declare module 'tdesign-react' { diff --git a/packages/components/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx similarity index 98% rename from packages/components/chatbot/_example/basic.tsx rename to packages/pro-components/chat/chatbot/_example/basic.tsx index 4385e414b0..2db6ce02d8 100644 --- a/packages/components/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -1,14 +1,16 @@ import React, { useEffect, useRef, useState } from 'react'; import { InternetIcon } from 'tdesign-icons-react'; -import type { +import { SSEChunkData, AIMessageContent, TdChatMessageConfigItem, RequestParams, ChatMessagesData, ChatServiceConfig, -} from 'tdesign-react'; -import { Button, ChatBot, Space, type TdChatbotApi } from 'tdesign-react'; + ChatBot, + type TdChatbotApi, +} from '@tdesign-react/aigc'; +import { Button, Space } from 'tdesign-react'; // 默认初始化消息 const mockData: ChatMessagesData[] = [ diff --git a/packages/components/chatbot/_example/code.tsx b/packages/pro-components/chat/chatbot/_example/code.tsx similarity index 97% rename from packages/components/chatbot/_example/code.tsx rename to packages/pro-components/chat/chatbot/_example/code.tsx index 036369da4d..83dda6ae30 100644 --- a/packages/components/chatbot/_example/code.tsx +++ b/packages/pro-components/chat/chatbot/_example/code.tsx @@ -1,12 +1,14 @@ import React, { useRef } from 'react'; -import type { +import type { Card, Space } from 'tdesign-react'; +import { + ChatBot, + ChatMessagesData, SSEChunkData, TdChatMessageConfig, AIMessageContent, RequestParams, ChatServiceConfig, -} from 'tdesign-react'; -import { Card, ChatBot, ChatMessagesData, DialogPlugin, type TdChatbotApi, Space } from 'tdesign-react'; +} from '@tdesign-react/aigc'; import Login from './components/login'; // 默认初始化消息 diff --git a/packages/components/chatbot/_example/components/login.tsx b/packages/pro-components/chat/chatbot/_example/components/login.tsx similarity index 100% rename from packages/components/chatbot/_example/components/login.tsx rename to packages/pro-components/chat/chatbot/_example/components/login.tsx diff --git a/packages/components/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx similarity index 100% rename from packages/components/chatbot/_example/custom.tsx rename to packages/pro-components/chat/chatbot/_example/custom.tsx diff --git a/packages/components/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx similarity index 100% rename from packages/components/chatbot/_example/hookComponent.tsx rename to packages/pro-components/chat/chatbot/_example/hookComponent.tsx diff --git a/packages/components/chatbot/_example/image.tsx b/packages/pro-components/chat/chatbot/_example/image.tsx similarity index 100% rename from packages/components/chatbot/_example/image.tsx rename to packages/pro-components/chat/chatbot/_example/image.tsx diff --git a/packages/components/chatbot/_example/index.css b/packages/pro-components/chat/chatbot/_example/index.css similarity index 100% rename from packages/components/chatbot/_example/index.css rename to packages/pro-components/chat/chatbot/_example/index.css diff --git a/packages/components/chatbot/_example/research.tsx b/packages/pro-components/chat/chatbot/_example/research.tsx similarity index 100% rename from packages/components/chatbot/_example/research.tsx rename to packages/pro-components/chat/chatbot/_example/research.tsx diff --git a/packages/components/chatbot/_example/searchContent.tsx b/packages/pro-components/chat/chatbot/_example/searchContent.tsx similarity index 100% rename from packages/components/chatbot/_example/searchContent.tsx rename to packages/pro-components/chat/chatbot/_example/searchContent.tsx diff --git a/packages/components/chatbot/_example/suggestionContent.tsx b/packages/pro-components/chat/chatbot/_example/suggestionContent.tsx similarity index 100% rename from packages/components/chatbot/_example/suggestionContent.tsx rename to packages/pro-components/chat/chatbot/_example/suggestionContent.tsx diff --git a/packages/components/chatbot/chatbot.en-US.md b/packages/pro-components/chat/chatbot/chatbot.en-US.md similarity index 100% rename from packages/components/chatbot/chatbot.en-US.md rename to packages/pro-components/chat/chatbot/chatbot.en-US.md diff --git a/packages/components/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md similarity index 88% rename from packages/components/chatbot/chatbot.md rename to packages/pro-components/chat/chatbot/chatbot.md index fab2a65c89..080f692117 100644 --- a/packages/components/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -2,21 +2,9 @@ title: Chatbot 智能对话 description: 智能对话聊天组件,适用于需要快速集成智能客服、问答系统等的AI应用 isComponent: true -usage: { title: '', description: '' } spline: navigation --- -## 基本用法 -### 标准化集成 -组件内置状态管理,SSE解析,自动处理消息内容渲染与交互逻辑,可开箱即用快速集成实现标准聊天界面。本示例演示了如何快速创建一个具备以下功能的智能对话组件: - - 初始化预设消息 - - 预设消息内容渲染支持(markdown、搜索、思考、建议等) - - 与服务端的SSE(Server-Sent Events)通信,支持流式消息响应 - - 自定义流式内容结构解析 - - 自定义请求参数处理 - - 常用消息操作处理及回调(复制、重试、点赞/点踩) - - 支持手动触发填入prompt, 重新生成,发送消息等 - {{ basic }} ### 组合式用法 diff --git a/packages/components/chatbot/index.ts b/packages/pro-components/chat/chatbot/index.ts similarity index 100% rename from packages/components/chatbot/index.ts rename to packages/pro-components/chat/chatbot/index.ts diff --git a/packages/components/chatbot/useChat.ts b/packages/pro-components/chat/chatbot/useChat.ts similarity index 100% rename from packages/components/chatbot/useChat.ts rename to packages/pro-components/chat/chatbot/useChat.ts diff --git a/packages/pro-components/chat/index.ts b/packages/pro-components/chat/index.ts new file mode 100644 index 0000000000..adc627eee6 --- /dev/null +++ b/packages/pro-components/chat/index.ts @@ -0,0 +1 @@ +export * from './chatbot'; diff --git a/packages/pro-components/chat/package.json b/packages/pro-components/chat/package.json new file mode 100644 index 0000000000..ae38c1480f --- /dev/null +++ b/packages/pro-components/chat/package.json @@ -0,0 +1,7 @@ +{ + "name": "@tdesign/pro-components-chat", + "private": true, + "main": "index.ts", + "author": "tdesign", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json new file mode 100644 index 0000000000..0310376d3d --- /dev/null +++ b/packages/tdesign-react-aigc/package.json @@ -0,0 +1,77 @@ +{ + "name": "@tdesign-react/aigc", + "version": "0.0.1", + "title": "@tdesign-react/aigc", + "description": "TDesign Pro Component for AIGC", + "module": "es/index.js", + "files": [ + "es", + "LICENSE", + "README.md", + "CHANGELOG.md" + ], + "sideEffects": [ + "site/*", + "es/**/style/**" + ], + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "start": "pnpm dev", + "dev": "vite", + "prebuild": "rimraf es/*", + "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && npm run build:tsc", + "build:tsc": "run-p build:tsc-*", + "build:tsc-es": "tsc --emitDeclarationOnly -d -p ./tsconfig.build.json --outDir es/", + "build:jsx-demo": "npm run generate:jsx-demo && npm run format:jsx-demo" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "lint-staged": { + "src/**/*.{ts,tsx}": [ + "prettier --write", + "npm run lint:fix" + ] + }, + "keywords": [ + "tdesign", + "react" + ], + "author": "tdesign", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + }, + "dependencies": { + "@babel/runtime": "~7.26.7", + "@popperjs/core": "~2.11.2", + "@tencent/tdesign-chatbot": "1.0.0-beta.47", + "@types/sortablejs": "^1.10.7", + "@types/tinycolor2": "^1.4.3", + "@types/validator": "^13.1.3", + "classnames": "~2.5.1", + "dayjs": "1.11.10", + "hoist-non-react-statics": "~3.3.2", + "lodash-es": "^4.17.21", + "mitt": "^3.0.0", + "raf": "~3.4.1", + "react-is": "^18.2.0", + "react-fast-compare":"^3.2.2", + "react-transition-group": "~4.4.1", + "sortablejs": "^1.15.0", + "tdesign-icons-react": "0.5.0", + "tinycolor2": "^1.4.2", + "tslib": "~2.3.1", + "validator": "~13.7.0" + }, + "devDependencies": { + "cors": "^2.8.5", + "tvision-charts-react": "^3.3.12", + "express": "^4.17.3" + } +} diff --git a/packages/tdesign-react-aigc/site/.gitignore b/packages/tdesign-react-aigc/site/.gitignore new file mode 100644 index 0000000000..082d756e2a --- /dev/null +++ b/packages/tdesign-react-aigc/site/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +package-lock.json \ No newline at end of file diff --git a/packages/tdesign-react-aigc/site/README.md b/packages/tdesign-react-aigc/site/README.md new file mode 100644 index 0000000000..45cb785a4e --- /dev/null +++ b/packages/tdesign-react-aigc/site/README.md @@ -0,0 +1,4 @@ +# tdesign-react + +- 为开发者提供组件文档 +- 支持本地组件开发 diff --git a/packages/tdesign-react-aigc/site/babel.config.demo.js b/packages/tdesign-react-aigc/site/babel.config.demo.js new file mode 100644 index 0000000000..40fab8500e --- /dev/null +++ b/packages/tdesign-react-aigc/site/babel.config.demo.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['@babel/preset-typescript'], +}; diff --git a/packages/tdesign-react-aigc/site/babel.config.js b/packages/tdesign-react-aigc/site/babel.config.js new file mode 100644 index 0000000000..142fdca948 --- /dev/null +++ b/packages/tdesign-react-aigc/site/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], + plugins: ['@babel/plugin-transform-runtime'], +}; diff --git a/packages/tdesign-react-aigc/site/docs/getting-started.md b/packages/tdesign-react-aigc/site/docs/getting-started.md new file mode 100644 index 0000000000..462070ad0c --- /dev/null +++ b/packages/tdesign-react-aigc/site/docs/getting-started.md @@ -0,0 +1,34 @@ +--- +title: TD AIGC Components for React +description: TDesign 适配桌面端的 AIGC 系列高阶组件库,适合在 React 技术栈中的AI chat组件。 +spline: ai +--- + +## 安装 + +### 使用 npm 安装 + +推荐使用 npm 方式进行开发 + +```shell +npm i @tdesign-react/aigc +``` + +## 使用 + +### 基础使用 + +推荐使用 Webpack 或 Rollup 等支持 tree-shaking 特性的构建工具,无需额外配置即可实现组件按需引入: + +```javascript +import { ChatBot } from '@tdesign-react/aigc'; +import '@tdesign-react/aigc/es/style/index.css'; // 少量公共样式 +``` + +## 浏览器兼容性 + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Edge >=84 | Firefox >=83 | Chrome >=84 | Safari >=14.1 | + +详情参见[桌面端组件库浏览器兼容性说明](https://github.com/Tencent/tdesign/wiki/%E6%A1%8C%E9%9D%A2%E7%AB%AF%E7%BB%84%E4%BB%B6%E5%BA%93%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E6%80%A7%E8%AF%B4%E6%98%8E) diff --git a/packages/tdesign-react-aigc/site/docs/sse.md b/packages/tdesign-react-aigc/site/docs/sse.md new file mode 100644 index 0000000000..0c2d671895 --- /dev/null +++ b/packages/tdesign-react-aigc/site/docs/sse.md @@ -0,0 +1,213 @@ +--- +title: 什么是流式输出 +order: 3 +group: + title: 快速上手 + order: 2 +--- + +## 简述 + +流式输出,也称为流式传输,指的是服务器持续地将数据推送到客户端,而不是一次性发送完毕。这种模式下,连接一旦建立,服务器就能实时地发送更新给客户端。 + +### 使用场景 + +流式输出的典型应用场景包括实时消息推送、股票行情更新、实时通知等,任何需要服务器向客户端实时传输数据的场合都可以使用。 + +### 与普通请求的区别 + +与传统的 HTTP 请求不同,普通请求是基于请求-响应模型,客户端发送请求后,服务器处理完毕即刻响应并关闭连接。流式输出则保持连接开放,允许服务器连续发送多个响应。 + +## 如何创建一个 SSE + +### Python + +在 Python 中,可以使用 fastAPI 框架来实现 Server-Sent Events。以下是一个示例: + +1. 安装 FastAPI 和 Uvicorn + 首先,确保你已经安装了 FastAPI 和 Uvicorn : + +``` +pip install fastapi uvicorn +``` + +2. 创建 FastAPI 应用 + 接下来,创建一个 FastAPI 应用,并定义一个流式接口。我们将使用异步生成器来逐步生成数据,并使用 StreamingResponse 来流式发送数据给客户端。 + +```js +import json +import asyncio +from fastapi import FastAPI +from sse_starlette.sse import EventSourceResponse +import uvicorn +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=['*'], # 设置允许跨域的域名列表,* 代表所有域名 + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) +async def event_generator(): + count = 0 + while True: + await asyncio.sleep(1) + count += 1 + data = {"count": count} + yield json.dumps(data) + +@app.get("/events") +async def get_events(): + return EventSourceResponse(event_generator()) +@app.post("/events") +async def post_events(): + return EventSourceResponse(event_generator()) + +if __name__ == '__main__': + uvicorn.run(app, host='0.0.0.0', port=4000) + +``` + +3. 运行应用 + 保存上述代码到一个文件(例如 main.py),然后运行应用: + +``` +python3 main.py +``` + +4. 测试流式接口 + +- get 接口 + +``` +curl http://0.0.0.0:4000/events +``` + +- post 接口 + +``` +curl -X POST "http://0.0.0.0:4000/events" -H "Content-Type: application/json" +``` + +你应该会看到每秒钟输出一行数据,类似于: + +``` +data: {"count": 1} + +data: {"count": 2} + +data: {"count": 3} + +data: {"count": 4} + +data: {"count": 5} + +... +``` + +## 为什么大模型 LLM 需要使用 SSE ? + +从某种意义上说,现阶段 LLM 模型采用 SSE 是历史遗留原因。 + +Transformer 前后内容是需要推理拼接的,且不说内容很多的时候,推理的时间会很长(还有 Max Token 的限制)。推理上下文的时候也是逐步推理生成的,因此默认就是流式输出进行包裹。如果哪天 AI 的速度可以不受这些内容的限制了,可能一次性返回是一个更好的交互。 + +## TDesign AI Chat 如何接入 SSE + +对于流式请求来说,组件其实只关心一个内容,那就是返回的 String,下面是 hunyuan 的流式返回案例。 + +```js +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"我是"}}],"usage":{"prompt_tokens":10,"completion_tokens":1,"total_tokens":11}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"由腾"}}],"usage":{"prompt_tokens":10,"completion_tokens":3,"total_tokens":13}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"讯公"}}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"司开"}}],"usage":{"prompt_tokens":10,"completion_tokens":7,"total_tokens":17}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"发的"}}],"usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"大型"}}],"usage":{"prompt_tokens":10,"completion_tokens":9,"total_tokens":19}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"语言"}}],"usage":{"prompt_tokens":10,"completion_tokens":10,"total_tokens":20}} + +data: {"id":"7eced65bb3cb122d9f927563fc0e5673","created":1695218378,"choices":[{"delta":{"role":"assistant","content":"模型"}}],"usage":{"prompt_tokens":10,"completion_tokens":11,"total_tokens":21}} +``` + +下面是解析流式接口请求的代码片段: + +```js +export const fetchSSE = async (options: FetchSSEOptions = {}) => { + const { success, fail, complete } = options; + // fetch请求流式接口url,需传入接口url和参数 + const responsePromise = fetch().catch((e) => { + const msg = e.toString() || '流式接口异常'; + complete?.(false, msg); + return Promise.reject(e); // 确保错误能够被后续的.catch()捕获 + }); + + responsePromise + .then((response) => { + if (!response?.ok) { + complete?.(false, response.statusText); + fail?.(); + throw new Error('Request failed'); // 抛出错误以便链式调用中的下一个.catch()处理 + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + if (!reader) throw new Error('No reader available'); + + const bufferArr: string[] = []; + let dataText = ''; // 记录数据 + const event: SSEEvent = { type: null, data: null }; + + async function processText({ done, value }: ReadableStreamReadResult): Promise { + if (done) { + complete?.(true); + return Promise.resolve(); + } + const chunk = decoder.decode(value); + const buffers = chunk.toString().split(/\r?\n/); + bufferArr.push(...buffers); + const i = 0; + while (i < bufferArr.length) { + const line = bufferArr[i]; + if (line) { + dataText += line; + const response = line.slice(6); + if (response === '[DONE]') { + event.type = 'finish'; + dataText = ''; + } else { + const choices = JSON.parse(response.trim())?.choices?.[0]; + if (choices.finish_reason === 'stop') { + event.type = 'finish'; + dataText = ''; + } else { + event.type = 'delta'; + event.data = choices; + } + } + } + if (event.type && event.data) { + const jsonData = JSON.parse(JSON.stringify(event)); + console.log('流式数据解析结果:', jsonData); + // 回调更新数据 + success(jsonData); + event.type = null; + event.data = null; + } + bufferArr.splice(i, 1); + } + return reader.read().then(processText); + } + + return reader.read().then(processText); + }) + .catch(() => { + // 处理整个链式调用过程中发生的任何错误 + fail?.(); + }); +}; +``` diff --git a/packages/tdesign-react-aigc/site/index.html b/packages/tdesign-react-aigc/site/index.html new file mode 100644 index 0000000000..d32dc653a5 --- /dev/null +++ b/packages/tdesign-react-aigc/site/index.html @@ -0,0 +1,31 @@ + + + + + + + TDesign Web React + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/packages/tdesign-react-aigc/site/package.json b/packages/tdesign-react-aigc/site/package.json new file mode 100644 index 0000000000..b8e1140019 --- /dev/null +++ b/packages/tdesign-react-aigc/site/package.json @@ -0,0 +1,43 @@ +{ + "name": "@tdesign/react-aigc-site", + "private": true, + "scripts": { + "start": "pnpm run dev", + "dev": "vite", + "build": "vite build", + "intranet": "vite build --mode intranet", + "preview": "vite build --mode preview && cp dist/index.html dist/404.html" + }, + "author": "tdesign", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.2.2", + "tdesign-icons-react": "^0.4.5", + "tdesign-react":"^1.12.1", + "tdesign-site-components": "^0.15.3", + "tdesign-theme-generator": "^1.1.3" + }, + "devDependencies": { + "@babel/core": "^7.16.5", + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.7.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/rimraf": "^4.0.5", + "@vitejs/plugin-react": "^4.3.1", + "camelcase": "^6.2.1", + "gray-matter": "^4.0.3", + "markdown-it-fence": "^0.1.3", + "semver": "^7.6.3", + "typescript": "5.6.2", + "vite": "^5.4.7", + "vite-plugin-istanbul": "^6.0.2", + "vite-plugin-pwa": "^0.20.5", + "vite-plugin-tdoc": "^2.0.4", + "vitest": "^2.1.1", + "workbox-precaching": "^7.0.0" + } +} diff --git a/packages/tdesign-react-aigc/site/playground.html b/packages/tdesign-react-aigc/site/playground.html new file mode 100644 index 0000000000..24fba7a740 --- /dev/null +++ b/packages/tdesign-react-aigc/site/playground.html @@ -0,0 +1,15 @@ + + + + + + TDesign Web React Playground + + + + + +
+ + + diff --git a/packages/tdesign-react-aigc/site/plugin-tdoc/demo.js b/packages/tdesign-react-aigc/site/plugin-tdoc/demo.js new file mode 100644 index 0000000000..c2a8e3c4e9 --- /dev/null +++ b/packages/tdesign-react-aigc/site/plugin-tdoc/demo.js @@ -0,0 +1,54 @@ +import path from 'path'; +import Markdownitfence from 'markdown-it-fence'; + +function mdInJsx(_md) { + return new Markdownitfence(_md, 'md_in_jsx', { + validate: () => true, + render(tokens, idx) { + const { content, info } = tokens[idx]; + return `
{\`${content.replace(
+        /`/g,
+        '\\`',
+      )}\`}
`; + }, + }); +} + +export default function renderDemo(md, container) { + md.use(mdInJsx).use(container, 'demo', { + validate(params) { + return params.trim().match(/^demo\s+([\\/.\w-]+)(\s+(.+?))?(\s+--dev)?$/); + }, + render(tokens, idx) { + if (tokens[idx].nesting === 1) { + const match = tokens[idx].info.trim().match(/^demo\s+([\\/.\w-]+)(\s+(.+?))?(\s+--dev)?$/); + const [, demoPath, componentName = ''] = match; + const demoPathOnlyLetters = demoPath.replace(/[^a-zA-Z\d]/g, ''); + const demoName = path.basename(demoPath).trim(); + const demoDefName = `Demo${demoPathOnlyLetters}`; + const demoCodeDefName = `Demo${demoPathOnlyLetters}Code`; + const demoJsxCodeDefName = `Demo${demoPathOnlyLetters}JsxCode`; + + const tpl = ` + +
+ + +
+
+
<${demoDefName} />
+
+
+ `; + + // eslint-disable-next-line no-param-reassign + tokens.tttpl = tpl; + + return `
`; + } + if (tokens.tttpl) return `${tokens.tttpl || ''}
`; + + return ''; + }, + }); +} diff --git a/packages/tdesign-react-aigc/site/plugin-tdoc/index.js b/packages/tdesign-react-aigc/site/plugin-tdoc/index.js new file mode 100644 index 0000000000..23740e9eb8 --- /dev/null +++ b/packages/tdesign-react-aigc/site/plugin-tdoc/index.js @@ -0,0 +1,25 @@ +import vitePluginTdoc from 'vite-plugin-tdoc'; + +import transforms from './transforms'; +import renderDemo from './demo'; + +export default () => vitePluginTdoc({ + transforms, // 解析 markdown 数据 + markdown: { + anchor: { + tabIndex: false, + config: (anchor) => ({ + permalink: anchor.permalink.linkInsideHeader({ symbol: '' }), + }), + }, + toc: { + listClass: 'tdesign-toc_list', + itemClass: 'tdesign-toc_list_item', + linkClass: 'tdesign-toc_list_item_a', + containerClass: 'tdesign-toc_container', + }, + container(md, container) { + renderDemo(md, container); + }, + }, +}); diff --git a/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js b/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js new file mode 100644 index 0000000000..f2c3ffbeeb --- /dev/null +++ b/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js @@ -0,0 +1,265 @@ +/* eslint-disable */ +import fs from 'fs'; +import path from 'path'; +import matter from 'gray-matter'; +import camelCase from 'camelcase'; +import { compileUsage, getGitTimestamp } from '../../../../packages/common/docs/compile'; + +import testCoverage from '../test-coverage'; + +import { transformSync } from '@babel/core'; + +export default async function mdToReact(options) { + const mdSegment = await customRender(options); + const { demoDefsStr, demoCodesDefsStr } = options; + + let coverage = {}; + if (mdSegment.isComponent) { + coverage = testCoverage[camelCase(mdSegment.componentName)] || {}; + } + + const reactSource = ` + import React, { useEffect, useRef, useState, useMemo } from 'react';\n + import { useLocation, useNavigate } from 'react-router-dom'; + import Prismjs from 'prismjs'; + import 'prismjs/components/prism-bash.js'; + import Stackblitz from '@tdesign/react-aigc-site/src/components/stackblitz/index.jsx'; + import Codesandbox from '@tdesign/react-aigc-site/src/components/codesandbox/index.jsx'; + ${demoDefsStr} + ${demoCodesDefsStr} + ${mdSegment.usage.importStr} + + function useQuery() { + return new URLSearchParams(useLocation().search); + } + + export default function TdDoc() { + const tdDocHeader = useRef(); + const tdDocTabs = useRef(); + + const isComponent = ${mdSegment.isComponent}; + + const location = useLocation(); + const navigate = useNavigate(); + + const query = useQuery(); + const [tab, setTab] = useState(query.get('tab') || 'demo'); + + const lastUpdated = useMemo(() => { + if (tab === 'design') return ${mdSegment.designDocLastUpdated}; + return ${mdSegment.lastUpdated}; + }, [tab]); + + useEffect(() => { + tdDocHeader.current.docInfo = { + title: \`${mdSegment.title}\`, + desc: \`${mdSegment.description}\` + } + + if (isComponent) { + tdDocTabs.current.tabs = ${JSON.stringify(mdSegment.tdDocTabs)}; + } + + document.title = \`${mdSegment.title} | TDesign\`; + + Prismjs.highlightAll(); + }, []); + + useEffect(() => { + if (!isComponent) return; + + const query = new URLSearchParams(location.search); + const currentTab = query.get('tab') || 'demo'; + setTab(currentTab); + tdDocTabs.current.tab = currentTab; + + tdDocTabs.current.onchange = ({ detail: currentTab }) => { + setTab(currentTab); + const query = new URLSearchParams(location.search); + if (query.get('tab') === currentTab) return; + navigate({ search: '?tab=' + currentTab }); + } + }, [location]) + + function isShow(currentTab) { + return currentTab === tab ? { display: 'block' } : { display: 'none' }; + } + + return ( + <> + ${ + mdSegment.tdDocHeader + ? ` + ${ + mdSegment.isComponent + ? ` + + + + ` + : '' + } + ` + : '' + } + { + isComponent ? ( + <> + +
+ ${mdSegment.demoMd.replace(/class=/g, 'className=')} + +
+
+
+ + ) :
${mdSegment.docMd.replace( + /class=/g, + 'className=', + )}
+ } +
+ +
+ + ) + } + `; + + const result = transformSync(reactSource, { + babelrc: false, + configFile: false, + sourceMaps: true, + generatorOpts: { + decoratorsBeforeExport: true, + }, + presets: [require('@babel/preset-react')], + plugins: [[require('@babel/plugin-transform-typescript'), { isTSX: true }]], + }); + + return { code: result.code, map: result.map }; +} + +const DEFAULT_TABS = [ + { tab: 'demo', name: '示例' }, + { tab: 'api', name: 'API' }, + { tab: 'design', name: '指南' }, +]; + +const DEFAULT_EN_TABS = [ + { tab: 'demo', name: 'DEMO' }, + { tab: 'api', name: 'API' }, + { tab: 'design', name: 'Guideline' }, +]; + +// 解析 markdown 内容 +async function customRender({ source, file, md }) { + let { content, data } = matter(source); + const lastUpdated = (await getGitTimestamp(file)) || Math.round(fs.statSync(file).mtimeMs); + // console.log('data', data); + const isEn = file.endsWith('en-US.md'); + // md top data + const pageData = { + spline: '', + toc: true, + title: '', + description: '', + isComponent: false, + tdDocHeader: true, + tdDocTabs: !isEn ? DEFAULT_TABS : DEFAULT_EN_TABS, + apiFlag: /#+\s*API/, + docClass: '', + lastUpdated, + designDocLastUpdated: lastUpdated, + ...data, + }; + + // md filename + const reg = file.match(/packages\/pro-components\/chat\/(\w+-?\w+)\/(\w+-?\w+)\.?(\w+-?\w+)?\.md/); + const componentName = reg && reg[1]; + + // split md + let [demoMd = '', apiMd = ''] = content.split(pageData.apiFlag); + + // fix table | render error + demoMd = demoMd.replace(/`([^`\r\n]+)`/g, (str, codeStr) => { + codeStr = codeStr.replace(/"/g, "'"); + return ``; + }); + + const mdSegment = { + ...pageData, + componentName, + usage: { importStr: '' }, + docMd: '', + demoMd: '', + apiMd: '', + designMd: '', + }; + + // 渲染 live demo + if (pageData.usage && pageData.isComponent) { + const usageObj = compileUsage({ + componentName, + usage: pageData.usage, + demoPath: path.posix.resolve(__dirname, `../../../pro-components/chat/${componentName}/_usage/index.jsx`), + }); + if (usageObj) { + mdSegment.usage = usageObj; + demoMd = `${usageObj.markdownStr} ${demoMd}`; + } + } + + if (pageData.isComponent) { + mdSegment.demoMd = md.render.call( + md, + `${pageData.toc ? '[toc]\n' : ''}${demoMd.replace(//g, '')}`, + ).html; + mdSegment.apiMd = md.render.call( + md, + `${pageData.toc ? '[toc]\n' : ''}${apiMd.replace(//g, '')}`, + ).html; + } else { + mdSegment.docMd = md.render.call( + md, + `${pageData.toc ? '[toc]\n' : ''}${content.replace(//g, '')}`, + ).html; + } + + // // 设计指南内容 不展示 design Tab 则不解析 + // if (pageData.isComponent && pageData.tdDocTabs.some((item) => item.tab === 'design')) { + // const designDocPath = path.resolve(__dirname, `../../../common/docs/web/design/${componentName}.md`); + + // if (fs.existsSync(designDocPath)) { + // const designDocLastUpdated = + // (await getGitTimestamp(designDocPath)) || Math.round(fs.statSync(designDocPath).mtimeMs); + // mdSegment.designDocLastUpdated = designDocLastUpdated; + + // const designMd = fs.readFileSync(designDocPath, 'utf-8'); + // mdSegment.designMd = md.render.call(md, `${pageData.toc ? '[toc]\n' : ''}${designMd}`).html; + // } else { + // console.log(`[vite-plugin-tdoc]: 未找到 ${designDocPath} 文件`); + // } + // } + + return mdSegment; +} diff --git a/packages/tdesign-react-aigc/site/plugin-tdoc/transforms.js b/packages/tdesign-react-aigc/site/plugin-tdoc/transforms.js new file mode 100644 index 0000000000..e66b0e35be --- /dev/null +++ b/packages/tdesign-react-aigc/site/plugin-tdoc/transforms.js @@ -0,0 +1,89 @@ +/* eslint-disable indent */ +/* eslint-disable no-param-reassign */ +import path from 'path'; +import fs from 'fs'; + +import mdToReact from './md-to-react'; + +let demoImports = {}; +let demoCodesImports = {}; + +export default { + before({ source, file }) { + const resourceDir = path.dirname(file); + console.log(resourceDir, 'resourceDir'); + const reg = file.match(/packages\/pro-components\/chat\/([\w-]+)\/(\w+-?\w+)\.?(\w+-?\w+)?\.md/); + + const fileName = reg && reg[0]; + const componentName = reg && reg[1]; + const localeName = reg && reg[3]; + + demoImports = {}; + demoCodesImports = {}; + // 统一换成 common 公共文档内容 + if (fileName && source.includes(':: BASE_DOC ::')) { + const localeDocPath = path.resolve(__dirname, `../../../${fileName}`); + const defaultDocPath = path.resolve( + __dirname, + `../../../common/docs/web/api/${localeName ? `${componentName}.${localeName}` : componentName}.md`, + ); + + let baseDoc = ''; + if (fs.existsSync(localeDocPath)) { + // 优先载入语言版本 + baseDoc = fs.readFileSync(localeDocPath, 'utf-8'); + } else if (fs.existsSync(defaultDocPath)) { + // 回退中文默认版本 + baseDoc = fs.readFileSync(defaultDocPath, 'utf-8'); + } else { + console.error(`未找到 ${defaultDocPath} 文件`); + } + source = source.replace(':: BASE_DOC ::', baseDoc); + } + + // 替换成对应 demo 文件 + source = source.replace(/\{\{\s+(.+)\s+\}\}/g, (demoStr, demoFileName) => { + const tsxDemoPath = path.resolve(resourceDir, `./_example/${demoFileName}.tsx`); + console.log(tsxDemoPath, 'tsxDemoPath'); + if (!fs.existsSync(tsxDemoPath)) { + console.log('\x1B[36m%s\x1B[0m', `${componentName} 组件需要实现 _example/${demoFileName}.tsx 示例!`); + return '\n

DEMO (🚧建设中)...

'; + } + + return `\n::: demo _example/${demoFileName} ${componentName}\n:::\n`; + }); + + source.replace(/:::\s*demo\s+([\\/.\w-]+)/g, (demoStr, relativeDemoPath) => { + const jsxDemoPath = `_example-js/${relativeDemoPath.split('/')?.[1]}`; + const demoPathOnlyLetters = relativeDemoPath.replace(/[^a-zA-Z\d]/g, ''); + const demoDefName = `Demo${demoPathOnlyLetters}`; + const demoJsxCodeDefName = `Demo${demoPathOnlyLetters}JsxCode`; + const demoCodeDefName = `Demo${demoPathOnlyLetters}Code`; + demoImports[demoDefName] = `import ${demoDefName} from './${relativeDemoPath}';`; + demoCodesImports[demoCodeDefName] = `import ${demoCodeDefName} from './${relativeDemoPath}?raw';`; + if (fs.existsSync(path.resolve(resourceDir, `${jsxDemoPath}.jsx`))) + demoCodesImports[demoJsxCodeDefName] = `import ${demoJsxCodeDefName} from './${jsxDemoPath}?raw'`; + else demoCodesImports[demoJsxCodeDefName] = `import ${demoJsxCodeDefName} from './${relativeDemoPath}?raw'`; + }); + + return source; + }, + render({ source, file, md }) { + const demoDefsStr = Object.keys(demoImports) + .map((key) => demoImports[key]) + .join('\n'); + const demoCodesDefsStr = Object.keys(demoCodesImports) + .map((key) => demoCodesImports[key]) + .join('\n'); + + const sfc = mdToReact({ + md, + file, + source, + demoDefsStr, + demoCodesDefsStr, + }); + + return sfc; + }, +}; diff --git a/packages/tdesign-react-aigc/site/public/apple-touch-icon.png b/packages/tdesign-react-aigc/site/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bef3ff5255f24b323c82ab9af534e29a191ed2df GIT binary patch literal 16077 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 literal 0 HcmV?d00001 diff --git a/packages/tdesign-react-aigc/site/public/favicon.ico b/packages/tdesign-react-aigc/site/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..086ac804a618e9bf16e23e7e5ce1407104c04add GIT binary patch literal 16958 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 I)r`RWH$?a1VgLXD literal 0 HcmV?d00001 diff --git a/packages/tdesign-react-aigc/site/public/logo.svg b/packages/tdesign-react-aigc/site/public/logo.svg new file mode 100644 index 0000000000..f2c84ac002 --- /dev/null +++ b/packages/tdesign-react-aigc/site/public/logo.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/tdesign-react-aigc/site/public/pwa-192x192.png b/packages/tdesign-react-aigc/site/public/pwa-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..06562a97a2900b1bc20d4347e9ea3a1176cd47f7 GIT binary patch literal 17924 zcmV*kKuf=gP)7Kkm(L+DFN05$ zs8N$=G)^E$-+StPRcozUt7_Li=XAq8ci-Fl@M)jB&mL>9RqJ2>8p>gFKF{>qU^hGo z4zB@l5P%stpz*(O&aizD|KDR^_L1M@xbR!}$UgF2wmXFX!ujYtx;<=j7{JG0m+`;h zZ`y}`b8rvc$>V*i@4_A{hJNTh=KK(|e$IKo&tVw0aXV(-DY&d3YD@--mtVU%p

_+EFP??#fD!e4@svL5xJ%?2YQh@MZ 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 literal 0 HcmV?d00001 diff --git a/packages/tdesign-react-aigc/site/public/pwa-512x512.png b/packages/tdesign-react-aigc/site/public/pwa-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..5a8085d057074f5d88e3a4038f8600ba943c2b4f GIT binary patch literal 103459 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 literal 0 HcmV?d00001 diff --git a/packages/tdesign-react-aigc/site/public/sw.js b/packages/tdesign-react-aigc/site/public/sw.js new file mode 100644 index 0000000000..67150b319c --- /dev/null +++ b/packages/tdesign-react-aigc/site/public/sw.js @@ -0,0 +1,8 @@ +import { precacheAndRoute } from 'workbox-precaching' + +precacheAndRoute(self.__WB_MANIFEST) + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') + self.skipWaiting() +}); diff --git a/packages/tdesign-react-aigc/site/pwaConfig.js b/packages/tdesign-react-aigc/site/pwaConfig.js new file mode 100644 index 0000000000..5ea8e0d6cc --- /dev/null +++ b/packages/tdesign-react-aigc/site/pwaConfig.js @@ -0,0 +1,25 @@ +export default { + strategies: 'injectManifest', + includeAssets: ['favicon.svg', 'favicon.ico', 'apple-touch-icon.png'], + injectManifest: { + maximumFileSizeToCacheInBytes: 7000000, + }, + manifest: { + name: 'TDesign for React', + short_name: 'TDesign', + description: 'React UI Component', + theme_color: '#ffffff', + icons: [ + { + src: 'pwa-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: 'pwa-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + ], + }, +}; diff --git a/packages/tdesign-react-aigc/site/site.config.mjs b/packages/tdesign-react-aigc/site/site.config.mjs new file mode 100644 index 0000000000..3207fcb348 --- /dev/null +++ b/packages/tdesign-react-aigc/site/site.config.mjs @@ -0,0 +1,75 @@ +export const docs = [ + { + title: '开始', + titleEn: 'Start', + type: 'doc', + children: [ + { + title: '快速开始', + titleEn: 'Getting Started', + name: 'getting-started', + path: '/react-aigc/getting-started', + component: () => import('./docs/getting-started.md'), + }, + { + title: '什么是流式输出', + titleEn: 'Getting Started', + name: 'getting-started', + path: '/react-aigc/sse', + component: () => import('./docs/sse.md'), + }, + ], + }, + { + title: '全局配置', + titleEn: 'Global Config', + type: 'doc', + children: [ + { + title: '自定义主题', + titleEn: 'Theme Customization', + name: 'custom-theme', + path: '/react-aigc/custom-theme', + component: () => import('@tdesign/common/theme.md'), + componentEn: () => import('@tdesign/common/theme.en-US.md'), + }, + { + title: '深色模式', + titleEn: 'Dark Mode', + name: 'dark-mode', + path: '/react-aigc/dark-mode', + component: () => import('@tdesign/common/dark-mode.md'), + componentEn: () => import('@tdesign/common/dark-mode.en-US.md'), + }, + ], + }, + { + title: 'AIGC', + titleEn: 'aigc', + type: 'component', + children: [ + { + title: 'Chatbot 智能对话', + titleEn: 'Chatbot', + name: 'chatbot', + path: '/react-aigc/components/chatbot', + component: () => import('@tdesign/pro-components-chat/chatbot/chatbot.md'), + componentEn: () => import('@tdesign/pro-components-chat/chatbot/chatbot.en-US.md'), + }, + ], + }, +]; + +const enDocs = docs.map((doc) => ({ + ...doc, + title: doc.titleEn, + children: doc?.children?.map((child) => ({ + title: child.titleEn, + name: `${child.name}-en`, + path: `${child.path}-en`, + meta: { lang: 'en' }, + component: child.componentEn || child.component, + })), +})); + +export default { docs, enDocs }; diff --git a/packages/tdesign-react-aigc/site/src/App.jsx b/packages/tdesign-react-aigc/site/src/App.jsx new file mode 100644 index 0000000000..ca4fc215db --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/App.jsx @@ -0,0 +1,154 @@ +import React, { useEffect, useRef, useState, lazy, Suspense } from 'react'; +import { BrowserRouter, Routes, Navigate, Route, useLocation, useNavigate, Outlet } from 'react-router-dom'; +import semver from 'semver'; +import Loading from '@tdesign/components/loading'; +import ConfigProvider from '@tdesign/components/config-provider'; +import zhConfig from '@tdesign/components/locale/zh_CN'; +import enConfig from '@tdesign/components/locale/en_US'; +import { getLang } from 'tdesign-site-components'; + +import packageJson from '../../package.json'; +import * as siteConfig from '../site.config'; +import { getRoute, filterVersions } from './utils'; + +const LazyDemo = lazy(() => import('./components/Demo')); + +const isDev = import.meta.env.DEV; + +const { docs, enDocs } = JSON.parse(JSON.stringify(siteConfig.default).replace(/component:.+/g, '')); + +const docsMap = { + zh: docs, + en: enDocs, +}; + +const registryUrl = 'https://service-edbzjd6y-1257786608.hk.apigw.tencentcs.com/release/npm/versions/tdesign-react'; +const currentVersion = packageJson.version.replace(/\./g, '_'); + +const docRoutes = [...getRoute(siteConfig.default.docs, []), ...getRoute(siteConfig.default.enDocs, [])]; +const renderRouter = docRoutes.map((nav, i) => { + const LazyCom = lazy(nav.component); + + return ( + }> + + + } + /> + ); +}); + +function Components() { + const location = useLocation(); + const navigate = useNavigate(); + + const tdHeaderRef = useRef(); + const tdDocAsideRef = useRef(); + const tdDocContentRef = useRef(); + const tdSelectRef = useRef(); + const tdDocSearch = useRef(); + + const [version] = useState(currentVersion); + const [globalConfig] = useState(() => (getLang() === 'en' ? enConfig : zhConfig)); + + function initHistoryVersions() { + fetch(registryUrl) + .then((res) => res.json()) + .then((res) => { + const options = []; + const versions = filterVersions(Object.keys(res.versions)); + + versions.forEach((v) => { + const nums = v.split('.'); + if (nums[0] === '0' && nums[1] < 21) return false; + + options.unshift({ label: v, value: v.replace(/\./g, '_') }); + }); + + tdSelectRef.current.options = options.sort((a, b) => (semver.gt(a.label, b.label) ? -1 : 1)); + }); + } + + useEffect(() => { + tdHeaderRef.current.framework = 'react'; + tdDocSearch.current.docsearchInfo = { indexName: 'tdesign_doc_react' }; + const isEn = window.location.pathname.endsWith('en'); + tdDocAsideRef.current.routerList = isEn ? docsMap.en : docsMap.zh; + + tdDocAsideRef.current.onchange = ({ detail }) => { + if (window.location.pathname === detail) return; + tdDocContentRef.current.pageStatus = 'hidden'; + navigate(detail); + requestAnimationFrame(() => { + tdDocContentRef.current.pageStatus = 'show'; + window.scrollTo(0, 0); + }); + }; + + tdSelectRef.current.onchange = ({ detail }) => { + const { value: version } = detail; + if (version === currentVersion) return; + + const historyUrl = `https://${version}-tdesign-react.surge.sh`; + window.open(historyUrl, '_blank'); + tdSelectRef.current.value = currentVersion; + }; + + if (isDev) return; + + initHistoryVersions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + document.querySelector('td-stats')?.track?.(); + }, [location]); + + return ( + + + + + + + + + + + + + + + + + ); +} + +function App() { + return ( + + + } /> + } /> + }> + + + } + /> + }> + {renderRouter} + + } /> + + + ); +} + +export default App; diff --git a/packages/tdesign-react-aigc/site/src/components/BaseUsage.jsx b/packages/tdesign-react-aigc/site/src/components/BaseUsage.jsx new file mode 100644 index 0000000000..9c422ca7a9 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/BaseUsage.jsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState, useRef } from 'react'; + +export function useConfigChange(configList) { + const defaultProps = configList.reduce((prev, curr) => { + if (curr.defaultValue) Object.assign(prev, { [curr.name]: curr.defaultValue }); + return prev; + }, {}); + + const [changedProps, setChangedProps] = useState(defaultProps); + + function onConfigChange(e) { + const { name, value } = e.detail; + + changedProps[name] = value; + setChangedProps({ ...changedProps }); + } + + return { + changedProps, + onConfigChange, + }; +} + +export function usePanelChange(panelList) { + const [panel, setPanel] = useState(panelList[0]?.value); + + function onPanelChange(e) { + const { value } = e.detail; + setPanel(value); + } + + return { + panel, + onPanelChange, + }; +} + +export default function BaseUsage(props) { + const { code, configList, panelList, onConfigChange, onPanelChange, children } = props; + const usageRef = useRef(); + + function handleConfigChange(e) { + onConfigChange?.(e); + } + + function handlePanelChange(e) { + onPanelChange?.(e); + } + + useEffect(() => { + usageRef.current.panelList = panelList; + usageRef.current.configList = configList; + usageRef.current.addEventListener('ConfigChange', handleConfigChange); + usageRef.current.addEventListener('PanelChange', handlePanelChange); + + return () => { + usageRef.current?.removeEventListener('ConfigChange', handleConfigChange); + usageRef.current?.removeEventListener('PanelChange', handlePanelChange); + }; + }, [configList, panelList]); + + useEffect(() => { + usageRef.current.code = code; + }, [code]); + + return ( + + {panelList.map((item) => ( +

+ {children} +
+ ))} + + ); +} diff --git a/packages/tdesign-react-aigc/site/src/components/Demo.jsx b/packages/tdesign-react-aigc/site/src/components/Demo.jsx new file mode 100644 index 0000000000..6e0625e3a8 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/Demo.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@tdesign/components'; +import { Link, useLocation } from 'react-router-dom'; + +export const demoFiles = import.meta.glob('../../../../components/**/_example/*.tsx', { eager: true }); + +const demoObject = {}; +Object.keys(demoFiles).forEach((key) => { + const match = key.match(/([\w-]+)._example.([\w-]+).tsx/); + const [, componentName, demoName] = match; + + demoObject[`${componentName}/${demoName}`] = demoFiles[key].default; + if (demoObject[componentName]) { + demoObject[componentName].push(demoName); + } else { + demoObject[componentName] = [demoName]; + } +}); + +export default function Demo() { + const location = useLocation(); + const match = location.pathname.match(/\/react\/demos\/([\w-]+)\/?([\w-]+)?/); + const [, componentName, demoName] = match; + + const demoList = demoObject[componentName]; + const demoFunc = demoObject[`${componentName}/${demoName}`]; + + return demoFunc ? ( + demoFunc() + ) : ( +
    + {demoList.map((demoName) => ( +
  • + + + +
  • + ))} +
+ ); +} diff --git a/packages/tdesign-react-aigc/site/src/components/Playground.jsx b/packages/tdesign-react-aigc/site/src/components/Playground.jsx new file mode 100644 index 0000000000..6c93d01816 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/Playground.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { HashRouter, Routes, Navigate, Route, useLocation, Link } from 'react-router-dom'; +import ReactDOM from 'react-dom'; +import { Button } from '@tdesign/components'; +import '@tdesign/components/style/index.js'; + +const demoFiles = import.meta.glob('../../../src/**/_example/*.jsx', { eager: true }); +const demoObject = {}; +const componentList = new Set(); +Object.keys(demoFiles).forEach((key) => { + const match = key.match(/([\w-]+)._example.([\w-]+).jsx/); + const [, componentName, demoName] = match; + + componentList.add(componentName); + demoObject[`${componentName}/${demoName}`] = demoFiles[key].default; + if (demoObject[componentName]) { + demoObject[componentName].push(demoName); + } else { + demoObject[componentName] = [demoName]; + } +}); + +function Demos() { + const location = useLocation(); + const match = location.pathname.match(/\/demos\/([\w-]+)\/?([\w-]+)?/); + const [, componentName] = match; + const demoList = demoObject[componentName]; + + return ( +
+
    + {[...componentList].map((com) => ( +
  • + + + +
  • + ))} +
+
+ {demoList.map((demoName) => ( +
+

{demoName}

+ {demoObject[`${componentName}/${demoName}`]()} +
+
+ ))} +
+
+ ); +} + +function App() { + return ( + + + } /> + } /> + + + ); +} + +ReactDOM.render( + + + , + document.getElementById('app'), +); diff --git a/packages/tdesign-react-aigc/site/src/components/codesandbox/content.js b/packages/tdesign-react-aigc/site/src/components/codesandbox/content.js new file mode 100644 index 0000000000..64fc9cb042 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/codesandbox/content.js @@ -0,0 +1,111 @@ +import orgPkg from '../../../package.json'; +import tdesignReactPkg from '../../../../package.json'; + +export const htmlContent = '
'; + +export const mainJsContent = ` + import React from 'react'; + import ReactDOM from 'react-dom'; + import Demo from './demo'; + import './index.css'; + import 'tdesign-react/es/style/index.css'; + + const rootElement = document.getElementById('app'); + ReactDOM.render(, rootElement); +`; + +export const styleContent = ` + /* 竖排展示 demo 行间距 16px */ + .tdesign-demo-block-column { + display: flex; + flex-direction: column; + row-gap: 16px; + } + + /* 竖排展示 demo 行间距 32px */ + .tdesign-demo-block-column-large { + display: flex; + flex-direction: column; + row-gap: 32px; + } + + /* 横排排展示 demo 列间距 16px */ + .tdesign-demo-block-row { + display: flex; + column-gap: 16px; + align-items: center; + } + + /* swiper 组件示例展示 */ + .tdesign-demo-block--swiper .demo-item { + display: flex; + height: 280px; + background-color: #4b5b76; + color: #fff; + justify-content: center; + align-items: center; + font-weight: 500; + font-size: 20px; + } +`; + +export const tsconfigContent = `{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} +`; + +export const pkgContent = JSON.stringify( + { + name: 'tdesign-react-demo', + version: '1.0.0', + description: 'React example starter project', + keywords: ['react', 'starter'], + main: 'src/main.tsx', + dependencies: { + react: orgPkg.dependencies.react, + 'react-dom': orgPkg.dependencies['react-dom'], + 'tdesign-react': tdesignReactPkg.version, + 'tdesign-icons-react': tdesignReactPkg.dependencies['tdesign-icons-react'], + '@types/react': orgPkg.devDependencies['@types/react'], + '@types/react-dom': orgPkg.devDependencies['@types/react-dom'], + dayjs: tdesignReactPkg.dependencies.dayjs, + 'lodash-es': tdesignReactPkg.dependencies['lodash-es'], + }, + devDependencies: { + typescript: '^4.4.4', + 'react-scripts': '^5.0.0', + }, + scripts: { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test --env=jsdom', + eject: 'react-scripts eject', + }, + browserslist: ['>0.2%', 'not dead', 'not ie <= 11', 'not op_mini all'], + }, + null, + 2, +); diff --git a/packages/tdesign-react-aigc/site/src/components/codesandbox/index.jsx b/packages/tdesign-react-aigc/site/src/components/codesandbox/index.jsx new file mode 100644 index 0000000000..43938e80b6 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/codesandbox/index.jsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { Tooltip, Loading } from '@tdesign/components'; + +import { mainJsContent, htmlContent, pkgContent, styleContent, tsconfigContent } from './content'; +import '../../styles/Codesandbox.less'; + +const TypeScriptType = 0; + +export default function Codesandbox(props) { + const [loading, setLoading] = useState(false); + + function onRunOnline() { + const demoDom = document.querySelector(`td-doc-demo[demo-name='${props.demoName}']`); + const code = demoDom?.currentRenderCode; + const isTypeScriptDemo = demoDom?.currentLangIndex === TypeScriptType; + setLoading(true); + if (isTypeScriptDemo) { + fetch('https://codesandbox.io/api/v1/sandboxes/define?json=1', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + files: { + 'package.json': { + content: pkgContent, + }, + 'public/index.html': { + content: htmlContent, + }, + 'src/main.tsx': { + content: mainJsContent, + }, + 'src/index.css': { + content: styleContent, + }, + 'src/demo.tsx': { + content: code, + }, + 'tsconfig.json': { + content: tsconfigContent, + }, + }, + }), + }) + .then((x) => x.json()) + .then(({ sandbox_id: sandboxId }) => { + window.open(`https://codesandbox.io/s/${sandboxId}?file=/src/demo.tsx`); + }) + .finally(() => { + setLoading(false); + }); + return; + } + fetch('https://codesandbox.io/api/v1/sandboxes/define?json=1', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + files: { + 'package.json': { + content: pkgContent, + }, + 'public/index.html': { + content: htmlContent, + }, + 'src/main.jsx': { + content: mainJsContent, + }, + 'src/index.css': { + content: styleContent, + }, + 'src/demo.jsx': { + content: code, + }, + }, + }), + }) + .then((x) => x.json()) + .then(({ sandbox_id: sandboxId }) => { + window.open(`https://codesandbox.io/s/${sandboxId}?file=/src/demo.jsx`); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( + +
+ +
+ + + + + + +
+
+
+
+ ); +} diff --git a/packages/tdesign-react-aigc/site/src/components/stackblitz/content.js b/packages/tdesign-react-aigc/site/src/components/stackblitz/content.js new file mode 100644 index 0000000000..f38bf26330 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/stackblitz/content.js @@ -0,0 +1,134 @@ +import orgPkg from '../../../package.json'; +import tdesignReactPkg from '../../../../package.json'; + +export const htmlContent = ` +
+ +`; + +export const mainJsContent = ` + import React, { StrictMode } from 'react'; + import { createRoot } from 'react-dom/client'; + + import Demo from './demo'; + import './index.css'; + import 'tdesign-react/dist/tdesign.css'; + + const rootElement = document.getElementById('app'); + const root = createRoot(rootElement); + + root.render( + + + , + ); +`; + +export const styleContent = ` + /* 竖排展示 demo 行间距 16px */ + .tdesign-demo-block-column { + display: flex; + flex-direction: column; + row-gap: 16px; + } + + /* 竖排展示 demo 行间距 32px */ + .tdesign-demo-block-column-large { + display: flex; + flex-direction: column; + row-gap: 32px; + } + + /* 横排排展示 demo 列间距 16px */ + .tdesign-demo-block-row { + display: flex; + column-gap: 16px; + align-items: center; + } + + /* swiper 组件示例展示 */ + .tdesign-demo-block--swiper .demo-item { + display: flex; + height: 280px; + background-color: #4b5b76; + color: #fff; + justify-content: center; + align-items: center; + font-weight: 500; + font-size: 20px; + } +`; + +export const tsconfigContent = `{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} +`; + +export const stackblitzRc = ` + { + "installDependencies": true, + "startCommand": "npm run dev" + } +`; + +export const viteConfigContent = ` + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + + export default defineConfig({ + plugins: [react()], + }); +`; + +export const packageJSONContent = JSON.stringify( + { + name: 'tdesign-react-demo', + version: '0.0.0', + private: true, + scripts: { + dev: 'vite', + build: 'vite build', + serve: 'vite preview', + }, + dependencies: { + react: orgPkg.dependencies.react, + 'react-dom': orgPkg.dependencies['react-dom'], + 'tdesign-react': tdesignReactPkg.version, + 'tdesign-icons-react': tdesignReactPkg.dependencies['tdesign-icons-react'], + '@types/react': orgPkg.devDependencies['@types/react'], + '@types/react-dom': orgPkg.devDependencies['@types/react-dom'], + dayjs: tdesignReactPkg.dependencies.dayjs, + 'lodash-es': tdesignReactPkg.dependencies['lodash-es'], + }, + devDependencies: { + vite: orgPkg.devDependencies.vite, + '@vitejs/plugin-react': orgPkg.devDependencies['@vitejs/plugin-react'], + typescript: orgPkg.devDependencies.typescript, + }, + }, + null, + 2, +); diff --git a/packages/tdesign-react-aigc/site/src/components/stackblitz/index.jsx b/packages/tdesign-react-aigc/site/src/components/stackblitz/index.jsx new file mode 100644 index 0000000000..8cb9c331a4 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/components/stackblitz/index.jsx @@ -0,0 +1,68 @@ +import React, { useRef, useState } from 'react'; +import { Tooltip } from '@tdesign/components'; + +import { + htmlContent, + mainJsContent, + styleContent, + tsconfigContent, + viteConfigContent, + packageJSONContent, + stackblitzRc, +} from './content'; + +const TypeScriptType = 0; + +export default function Stackblitz(props) { + const formRef = useRef(null); + const [isTypeScriptDemo, setIsTypeScriptDemo] = useState(false); + const [code, setCurrentCode] = useState(''); + function submit() { + const demoDom = document.querySelector(`td-doc-demo[demo-name='${props.demoName}']`); + const isTypeScriptDemo = demoDom?.currentLangIndex === TypeScriptType; + + setCurrentCode(demoDom?.currentRenderCode); + setIsTypeScriptDemo(isTypeScriptDemo); + + setTimeout(() => { + formRef.current.submit(); + }); + } + + return ( + +
+ {isTypeScriptDemo ? ( + <> + + + + ) : ( + + )} + + + + + + + + +
+ + + +
+
+
+ ); +} diff --git a/packages/tdesign-react-aigc/site/src/main.jsx b/packages/tdesign-react-aigc/site/src/main.jsx new file mode 100644 index 0000000000..80721dc442 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/main.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { registerLocaleChange } from 'tdesign-site-components'; +import App from './App'; + +// import tdesign style; +import '@tdesign/components/style/index.js'; +import '@tdesign/common-style/web/docs.less'; + +import 'tdesign-site-components/lib/styles/style.css'; +import 'tdesign-site-components/lib/styles/prism-theme.less'; +import 'tdesign-site-components/lib/styles/prism-theme-dark.less'; + +import 'tdesign-icons-view'; + +import 'tdesign-theme-generator'; + +const rootElement = document.getElementById('app'); +const root = createRoot(rootElement); + +registerLocaleChange(); + +root.render( + + + , +); diff --git a/packages/tdesign-react-aigc/site/src/pwa.js b/packages/tdesign-react-aigc/site/src/pwa.js new file mode 100644 index 0000000000..3c1442b732 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/pwa.js @@ -0,0 +1,10 @@ +import { registerSW } from 'virtual:pwa-register'; + +const updateSW = registerSW({ + onNeedRefresh() { + console.log('onNeedRefresh'); + }, + onOfflineReady() { + console.log('onOfflineReady'); + }, +}); diff --git a/packages/tdesign-react-aigc/site/src/styles/Codesandbox.less b/packages/tdesign-react-aigc/site/src/styles/Codesandbox.less new file mode 100644 index 0000000000..c2b54f6fa3 --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/styles/Codesandbox.less @@ -0,0 +1,24 @@ +div[slot='action'] { + display: inline-flex; + column-gap: 8px; +} + +.action-online { + width: 32px; + height: 32px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px; + transition: all 0.2s linear; + cursor: pointer; + border-radius: 3px; + color: var(--text-secondary); + + &:hover { + color: var(--text-primary); + background-color: var(--bg-color-demo-hover, rgb(243, 243, 243)); + } +} + diff --git a/packages/tdesign-react-aigc/site/src/sw.js b/packages/tdesign-react-aigc/site/src/sw.js new file mode 100644 index 0000000000..67150b319c --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/sw.js @@ -0,0 +1,8 @@ +import { precacheAndRoute } from 'workbox-precaching' + +precacheAndRoute(self.__WB_MANIFEST) + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') + self.skipWaiting() +}); diff --git a/packages/tdesign-react-aigc/site/src/utils.js b/packages/tdesign-react-aigc/site/src/utils.js new file mode 100644 index 0000000000..586b03360f --- /dev/null +++ b/packages/tdesign-react-aigc/site/src/utils.js @@ -0,0 +1,24 @@ +export function getRoute(list, docRoutes) { + list.forEach((item) => { + if (item.children) { + return getRoute(item.children, docRoutes); + } + return docRoutes.push(item); + }); + return docRoutes; +} + +// 过滤小版本号 +export function filterVersions(versions = []) { + const versionMap = new Map(); + + versions.forEach((v) => { + if (v.includes('-')) return false; + const nums = v.split('.'); + versionMap.set(`${nums[0]}.${nums[1]}`, v); + }); + + return [...versionMap.values()].sort((a, b) => { + return Number(a.split('.').slice(0, 2).join('.')) - Number(b.split('.').slice(0, 2).join('.')); + }); +} diff --git a/packages/tdesign-react-aigc/site/test-coverage.js b/packages/tdesign-react-aigc/site/test-coverage.js new file mode 100644 index 0000000000..e1e1b27f39 --- /dev/null +++ b/packages/tdesign-react-aigc/site/test-coverage.js @@ -0,0 +1,446 @@ +module.exports = { + "Util": { + "statements": "58.47%", + "branches": "47.92%", + "functions": "63.49%", + "lines": "60.29%" + }, + "affix": { + "statements": "84.84%", + "branches": "61.29%", + "functions": "87.5%", + "lines": "85.93%" + }, + "alert": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "anchor": { + "statements": "93.85%", + "branches": "69.56%", + "functions": "88%", + "lines": "98.05%" + }, + "autoComplete": { + "statements": "96.17%", + "branches": "90.9%", + "functions": "97.05%", + "lines": "97.95%" + }, + "avatar": { + "statements": "92.64%", + "branches": "86.48%", + "functions": "75%", + "lines": "92.64%" + }, + "backTop": { + "statements": "78.68%", + "branches": "53.84%", + "functions": "83.33%", + "lines": "83.92%" + }, + "badge": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "breadcrumb": { + "statements": "84.31%", + "branches": "53.12%", + "functions": "85.71%", + "lines": "89.58%" + }, + "button": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "calendar": { + "statements": "78.07%", + "branches": "53.24%", + "functions": "72%", + "lines": "80.86%" + }, + "card": { + "statements": "100%", + "branches": "84.61%", + "functions": "100%", + "lines": "100%" + }, + "cascader": { + "statements": "93.12%", + "branches": "75.8%", + "functions": "90.62%", + "lines": "94.21%" + }, + "checkbox": { + "statements": "90.27%", + "branches": "83.01%", + "functions": "100%", + "lines": "91.3%" + }, + "collapse": { + "statements": "96.15%", + "branches": "78.94%", + "functions": "94.11%", + "lines": "96.1%" + }, + "colorPicker": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "comment": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "common": { + "statements": "94.33%", + "branches": "84.61%", + "functions": "100%", + "lines": "97.95%" + }, + "configProvider": { + "statements": "70.58%", + "branches": "66.66%", + "functions": "25%", + "lines": "68.75%" + }, + "datePicker": { + "statements": "59.16%", + "branches": "41.42%", + "functions": "58.9%", + "lines": "62.07%" + }, + "descriptions": { + "statements": "98.82%", + "branches": "100%", + "functions": "95.45%", + "lines": "100%" + }, + "dialog": { + "statements": "83.53%", + "branches": "71.92%", + "functions": "79.06%", + "lines": "85.62%" + }, + "divider": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "drawer": { + "statements": "86.44%", + "branches": "84.48%", + "functions": "61.53%", + "lines": "89.09%" + }, + "dropdown": { + "statements": "89.28%", + "branches": "58.69%", + "functions": "80%", + "lines": "92.59%" + }, + "empty": { + "statements": "84.37%", + "branches": "63.63%", + "functions": "100%", + "lines": "84.37%" + }, + "form": { + "statements": "83.5%", + "branches": "70.73%", + "functions": "81.51%", + "lines": "87.17%" + }, + "grid": { + "statements": "84.21%", + "branches": "74.24%", + "functions": "90%", + "lines": "84.21%" + }, + "guide": { + "statements": "99.32%", + "branches": "92.85%", + "functions": "100%", + "lines": "99.31%" + }, + "hooks": { + "statements": "61.17%", + "branches": "49.41%", + "functions": "68.86%", + "lines": "62.4%" + }, + "image": { + "statements": "88.88%", + "branches": "82.53%", + "functions": "80%", + "lines": "91.86%" + }, + "imageViewer": { + "statements": "65.28%", + "branches": "76.54%", + "functions": "65.11%", + "lines": "65.59%" + }, + "input": { + "statements": "93.9%", + "branches": "92.3%", + "functions": "89.47%", + "lines": "94.19%" + }, + "inputAdornment": { + "statements": "86.95%", + "branches": "54.54%", + "functions": "100%", + "lines": "90.47%" + }, + "inputNumber": { + "statements": "76.74%", + "branches": "59.74%", + "functions": "78.94%", + "lines": "80.16%" + }, + "layout": { + "statements": "91.48%", + "branches": "41.66%", + "functions": "85.71%", + "lines": "91.48%" + }, + "link": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "list": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "loading": { + "statements": "86.07%", + "branches": "65%", + "functions": "78.57%", + "lines": "89.33%" + }, + "locale": { + "statements": "73.07%", + "branches": "72.22%", + "functions": "83.33%", + "lines": "73.91%" + }, + "menu": { + "statements": "85.44%", + "branches": "69.23%", + "functions": "81.48%", + "lines": "90.51%" + }, + "message": { + "statements": "88.96%", + "branches": "86.66%", + "functions": "70.45%", + "lines": "94.28%" + }, + "notification": { + "statements": "89.24%", + "branches": "75%", + "functions": "86.95%", + "lines": "92.59%" + }, + "pagination": { + "statements": "93.82%", + "branches": "76.08%", + "functions": "93.75%", + "lines": "94.87%" + }, + "popconfirm": { + "statements": "76.92%", + "branches": "60%", + "functions": "81.81%", + "lines": "76.92%" + }, + "popup": { + "statements": "48.38%", + "branches": "44.77%", + "functions": "45.23%", + "lines": "46.52%" + }, + "progress": { + "statements": "89.23%", + "branches": "65.71%", + "functions": "100%", + "lines": "89.23%" + }, + "radio": { + "statements": "83.58%", + "branches": "47.36%", + "functions": "92.85%", + "lines": "83.58%" + }, + "rangeInput": { + "statements": "75.32%", + "branches": "62.79%", + "functions": "51.85%", + "lines": "75%" + }, + "rate": { + "statements": "96.36%", + "branches": "80.76%", + "functions": "100%", + "lines": "96.36%" + }, + "select": { + "statements": "100%", + "branches": "100%", + "functions": "100%", + "lines": "100%" + }, + "selectInput": { + "statements": "99%", + "branches": "94.11%", + "functions": "100%", + "lines": "100%" + }, + "skeleton": { + "statements": "77.35%", + "branches": "43.47%", + "functions": "83.33%", + "lines": "78.84%" + }, + "slider": { + "statements": "89.47%", + "branches": "68.85%", + "functions": "92.85%", + "lines": "91.2%" + }, + "space": { + "statements": "87.75%", + "branches": "84.37%", + "functions": "100%", + "lines": "87.75%" + }, + "statistic": { + "statements": "84.44%", + "branches": "85.71%", + "functions": "72.72%", + "lines": "85.71%" + }, + "steps": { + "statements": "87.8%", + "branches": "66.07%", + "functions": "100%", + "lines": "87.8%" + }, + "swiper": { + "statements": "71.93%", + "branches": "43.28%", + "functions": "85.71%", + "lines": "71.35%" + }, + "switch": { + "statements": "96.55%", + "branches": "92%", + "functions": "100%", + "lines": "96.55%" + }, + "table": { + "statements": "48.36%", + "branches": "33.74%", + "functions": "45.91%", + "lines": "49.56%" + }, + "tabs": { + "statements": "89.79%", + "branches": "77.27%", + "functions": "88%", + "lines": "90.86%" + }, + "tag": { + "statements": "56.25%", + "branches": "48.21%", + "functions": "47.05%", + "lines": "55.55%" + }, + "tagInput": { + "statements": "85.11%", + "branches": "82.89%", + "functions": "84.21%", + "lines": "87.26%" + }, + "textarea": { + "statements": "82.89%", + "branches": "62.22%", + "functions": "80.95%", + "lines": "86.76%" + }, + "timePicker": { + "statements": "82.88%", + "branches": "73.68%", + "functions": "86.36%", + "lines": "83.01%" + }, + "timeline": { + "statements": "96.87%", + "branches": "88.13%", + "functions": "90.9%", + "lines": "96.77%" + }, + "tooltip": { + "statements": "90.74%", + "branches": "64.7%", + "functions": "75%", + "lines": "90.56%" + }, + "transfer": { + "statements": "86.27%", + "branches": "67.61%", + "functions": "84.28%", + "lines": "87.97%" + }, + "tree": { + "statements": "86.22%", + "branches": "70.64%", + "functions": "84.9%", + "lines": "88.33%" + }, + "treeSelect": { + "statements": "95.45%", + "branches": "82.35%", + "functions": "97.56%", + "lines": "97.2%" + }, + "typography": { + "statements": "95.52%", + "branches": "76.31%", + "functions": "81.81%", + "lines": "98.43%" + }, + "upload": { + "statements": "96.77%", + "branches": "95.65%", + "functions": "88.88%", + "lines": "100%" + }, + "watermark": { + "statements": "95.77%", + "branches": "79.41%", + "functions": "100%", + "lines": "98.5%" + }, + "utils": { + "statements": "75.43%", + "branches": "73.68%", + "functions": "83.33%", + "lines": "74.54%" + } +}; diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js new file mode 100644 index 0000000000..bd16e24e6f --- /dev/null +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -0,0 +1,56 @@ +import path from 'path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { VitePWA } from 'vite-plugin-pwa'; +import pwaConfig from './pwaConfig'; +import tdocPlugin from './plugin-tdoc'; + +const publicPathMap = { + preview: '/', + intranet: '/react-aigc/', + production: 'https://static.tdesign.tencent.com/react-aigc/', +}; + +const disableTreeShakingPlugin = (paths) => ({ + name: 'disable-treeshake', + transform(code, id) { + for (const path of paths) { + if (id.includes(path)) { + return { code, map: null, moduleSideEffects: 'no-treeshake' }; + } + } + }, +}); + +export default ({ mode }) => + defineConfig({ + base: publicPathMap[mode], + resolve: { + alias: { + '@tdesign-react/aigc': path.resolve(__dirname, '../../pro-components/chat'), + 'tdesign-react': path.resolve(__dirname, '../../components'), + }, + }, + build: { + rollupOptions: { + input: { + index: 'index.html', + playground: 'playground.html', + }, + }, + }, + jsx: 'react', + server: { + host: '0.0.0.0', + port: 15001, + open: '/', + https: false, + fs: { + strict: false, + }, + }, + test: { + environment: 'jsdom', + }, + plugins: [react(), tdocPlugin(), VitePWA(pwaConfig), disableTreeShakingPlugin(['style/'])], + }); diff --git a/packages/tdesign-react/site/site.config.mjs b/packages/tdesign-react/site/site.config.mjs index 2d6e9c6c84..cb1ae34f5a 100644 --- a/packages/tdesign-react/site/site.config.mjs +++ b/packages/tdesign-react/site/site.config.mjs @@ -68,86 +68,6 @@ export const docs = [ }, ], }, - { - title: 'AIGC', - titleEn: 'aigc', - type: 'component', - children: [ - { - title: 'Chatbot 智能对话', - titleEn: 'Chatbot', - name: 'chatbot', - path: '/react/components/chatbot', - component: () => import('@tdesign/components/chatbot/chatbot.md'), - componentEn: () => import('@tdesign/components/chatbot/chatbot.en-US.md'), - }, - { - title: 'ChatSender 对话输入', - titleEn: 'ChatSender', - name: 'chat-sender', - path: '/react/components/chat-sender', - component: () => import('@tdesign/components/chat-sender/chat-sender.md'), - componentEn: () => import('@tdesign/components/chat-sender/chat-sender.en-US.md'), - }, - { - title: 'ChatMessage 对话消息体', - titleEn: 'ChatMessage', - name: 'chat-message', - path: '/react/components/chat-message', - component: () => import('@tdesign/components/chat-message/chat-message.md'), - componentEn: () => import('@tdesign/components/chat-message/chat-message.en-US.md'), - }, - { - title: 'ChatActionBar 对话操作栏', - titleEn: 'ChatActionBar', - name: 'chat-actionbar', - path: '/react/components/chat-actionbar', - component: () => import('@tdesign/components/chat-actionbar/chat-actionbar.md'), - componentEn: () => import('@tdesign/components/chat-actionbar/chat-actionbar.en-US.md'), - }, - { - title: 'ChatMarkdown 消息内容', - titleEn: 'ChatMarkdown', - name: 'chat-markdown', - path: '/react/components/chat-markdown', - component: () => import('@tdesign/components/chat-markdown/chat-markdown.md'), - componentEn: () => import('@tdesign/components/chat-markdown/chat-markdown.en-US.md'), - }, - { - title: 'ChatThinking 思考过程', - titleEn: 'ChatThinking', - name: 'chat-thinking', - path: '/react/components/chat-thinking', - component: () => import('@tdesign/components/chat-thinking/chat-thinking.md'), - componentEn: () => import('@tdesign/components/chat-thinking/chat-thinking.en-US.md'), - }, - - { - title: 'ChatLoading 对话加载', - titleEn: 'ChatLoading', - name: 'chat-loading', - path: '/react/components/chat-loading', - component: () => import('@tdesign/components/chat-loading/chat-loading.md'), - componentEn: () => import('@tdesign/components/chat-loading/chat-loading.en-US.md'), - }, - { - title: 'FileCard 文件缩略卡片', - titleEn: 'FileCard', - name: 'filecard', - path: '/react/components/chat-filecard', - component: () => import('@tdesign/components/chat-filecard/chat-filecard.md'), - componentEn: () => import('@tdesign/components/chat-filecard/chat-filecard.en-US.md'), - }, - { - title: 'ChatAttachments 附件列表', - titleEn: 'ChatAttachments', - name: 'chat-attachment', - path: '/react/components/chat-attachment', - component: () => import('@tdesign/components/chat-attachment/chat-attachment.md'), - componentEn: () => import('@tdesign/components/chat-attachment/chat-attachment.en-US.md'), - }, - ], - }, { title: '基础', titleEn: 'Base', diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 08f4a78ef1..6e89016176 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,3 @@ packages: - 'packages/**' - - 'site' - - 'test' \ No newline at end of file + - 'test' From 931743b43dcf49514305cf12549d3d2924a3216a Mon Sep 17 00:00:00 2001 From: Uyarn Date: Mon, 19 May 2025 23:54:53 +0800 Subject: [PATCH 066/228] refactor: aigc site --- .../chat}/chat-actionbar/_example/base.tsx | 3 +- .../chat}/chat-actionbar/_example/custom.tsx | 3 +- .../chat-actionbar/chat-actionbar.en-US.md | 0 .../chat}/chat-actionbar/chat-actionbar.md | 0 .../chat}/chat-actionbar/index.ts | 0 .../chat/chat-attachments}/_example/base.tsx | 4 +- .../chat/chat-attachments}/_usage/index.jsx | 11 +-- .../chat/chat-attachments}/_usage/props.json | 0 .../chat-attachments.en-US.md} | 0 .../chat-attachments/chat-attachments.md} | 2 +- .../chat/chat-attachments}/index.ts | 0 .../chat}/chat-filecard/_example/base.tsx | 3 +- .../chat-filecard/chat-filecard.en-US.md | 0 .../chat}/chat-filecard/chat-filecard.md | 0 .../chat}/chat-filecard/index.ts | 0 .../chat}/chat-loading/_example/base.tsx | 3 +- .../chat}/chat-loading/chat-loading.en-US.md | 0 .../chat}/chat-loading/chat-loading.md | 0 .../chat}/chat-loading/index.ts | 0 .../chat}/chat-markdown/_example/base.tsx | 4 +- .../chat}/chat-markdown/_example/custom.tsx | 0 .../chat}/chat-markdown/_example/mock.md | 0 .../chat}/chat-markdown/_example/plugin.tsx | 3 +- .../chat-markdown/chat-markdown.en-US.md | 0 .../chat}/chat-markdown/chat-markdown.md | 0 .../chat}/chat-markdown/index.ts | 0 .../chat}/chat-message/_example/action.tsx | 4 +- .../chat}/chat-message/_example/base.tsx | 3 +- .../chat}/chat-message/_example/configure.tsx | 3 +- .../chat}/chat-message/_example/content.tsx | 4 +- .../chat}/chat-message/_example/custom.tsx | 4 +- .../chat}/chat-message/_example/status.tsx | 3 +- .../chat}/chat-message/chat-message.en-US.md | 0 .../chat}/chat-message/chat-message.md | 0 .../chat}/chat-message/index.ts | 0 .../chat}/chat-sender/_example/attachment.tsx | 5 +- .../chat}/chat-sender/_example/base.tsx | 2 +- .../chat}/chat-sender/_example/custom.tsx | 3 +- .../chat}/chat-sender/_example/style.css | 0 .../chat}/chat-sender/chat-sender.en-US.md | 0 .../chat}/chat-sender/chat-sender.md | 0 .../chat}/chat-sender/index.ts | 0 .../chat}/chat-thinking/_example/base.tsx | 2 +- .../chat}/chat-thinking/_example/style.tsx | 18 ++--- .../chat-thinking/chat-thinking.en-US.md | 0 .../chat}/chat-thinking/chat-thinking.md | 0 .../chat}/chat-thinking/index.ts | 0 .../chat/chatbot/_example/custom.tsx | 8 ++- .../chat/chatbot/_example/hookComponent.tsx | 2 +- .../chat/chatbot/_example/image.tsx | 10 +-- .../chat/chatbot/_example/research.tsx | 8 +-- .../chat/chatbot/_example/searchContent.tsx | 2 +- .../chatbot/_example/suggestionContent.tsx | 2 +- packages/pro-components/chat/index.ts | 8 +++ .../tdesign-react-aigc/site/site.config.mjs | 69 ++++++++++++++++++- tsconfig.json | 3 + 56 files changed, 143 insertions(+), 56 deletions(-) rename packages/{components => pro-components/chat}/chat-actionbar/_example/base.tsx (79%) rename packages/{components => pro-components/chat}/chat-actionbar/_example/custom.tsx (78%) rename packages/{components => pro-components/chat}/chat-actionbar/chat-actionbar.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-actionbar/chat-actionbar.md (100%) rename packages/{components => pro-components/chat}/chat-actionbar/index.ts (100%) rename packages/{components/chat-attachment => pro-components/chat/chat-attachments}/_example/base.tsx (91%) rename packages/{components/chat-attachment => pro-components/chat/chat-attachments}/_usage/index.jsx (89%) rename packages/{components/chat-attachment => pro-components/chat/chat-attachments}/_usage/props.json (100%) rename packages/{components/chat-attachment/chat-attachment.en-US.md => pro-components/chat/chat-attachments/chat-attachments.en-US.md} (100%) rename packages/{components/chat-attachment/chat-attachment.md => pro-components/chat/chat-attachments/chat-attachments.md} (99%) rename packages/{components/chat-attachment => pro-components/chat/chat-attachments}/index.ts (100%) rename packages/{components => pro-components/chat}/chat-filecard/_example/base.tsx (91%) rename packages/{components => pro-components/chat}/chat-filecard/chat-filecard.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-filecard/chat-filecard.md (100%) rename packages/{components => pro-components/chat}/chat-filecard/index.ts (100%) rename packages/{components => pro-components/chat}/chat-loading/_example/base.tsx (84%) rename packages/{components => pro-components/chat}/chat-loading/chat-loading.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-loading/chat-loading.md (100%) rename packages/{components => pro-components/chat}/chat-loading/index.ts (100%) rename packages/{components => pro-components/chat}/chat-markdown/_example/base.tsx (96%) rename packages/{components => pro-components/chat}/chat-markdown/_example/custom.tsx (100%) rename packages/{components => pro-components/chat}/chat-markdown/_example/mock.md (100%) rename packages/{components => pro-components/chat}/chat-markdown/_example/plugin.tsx (93%) rename packages/{components => pro-components/chat}/chat-markdown/chat-markdown.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-markdown/chat-markdown.md (100%) rename packages/{components => pro-components/chat}/chat-markdown/index.ts (100%) rename packages/{components => pro-components/chat}/chat-message/_example/action.tsx (90%) rename packages/{components => pro-components/chat}/chat-message/_example/base.tsx (83%) rename packages/{components => pro-components/chat}/chat-message/_example/configure.tsx (93%) rename packages/{components => pro-components/chat}/chat-message/_example/content.tsx (97%) rename packages/{components => pro-components/chat}/chat-message/_example/custom.tsx (95%) rename packages/{components => pro-components/chat}/chat-message/_example/status.tsx (90%) rename packages/{components => pro-components/chat}/chat-message/chat-message.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-message/chat-message.md (100%) rename packages/{components => pro-components/chat}/chat-message/index.ts (100%) rename packages/{components => pro-components/chat}/chat-sender/_example/attachment.tsx (93%) rename packages/{components => pro-components/chat}/chat-sender/_example/base.tsx (94%) rename packages/{components => pro-components/chat}/chat-sender/_example/custom.tsx (97%) rename packages/{components => pro-components/chat}/chat-sender/_example/style.css (100%) rename packages/{components => pro-components/chat}/chat-sender/chat-sender.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-sender/chat-sender.md (100%) rename packages/{components => pro-components/chat}/chat-sender/index.ts (100%) rename packages/{components => pro-components/chat}/chat-thinking/_example/base.tsx (97%) rename packages/{components => pro-components/chat}/chat-thinking/_example/style.tsx (92%) rename packages/{components => pro-components/chat}/chat-thinking/chat-thinking.en-US.md (100%) rename packages/{components => pro-components/chat}/chat-thinking/chat-thinking.md (100%) rename packages/{components => pro-components/chat}/chat-thinking/index.ts (100%) diff --git a/packages/components/chat-actionbar/_example/base.tsx b/packages/pro-components/chat/chat-actionbar/_example/base.tsx similarity index 79% rename from packages/components/chat-actionbar/_example/base.tsx rename to packages/pro-components/chat/chat-actionbar/_example/base.tsx index b7c841cf04..172d99bcf3 100644 --- a/packages/components/chat-actionbar/_example/base.tsx +++ b/packages/pro-components/chat/chat-actionbar/_example/base.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Space, ChatActionBar } from 'tdesign-react'; +import { Space } from 'tdesign-react'; +import { ChatActionBar } from '@tdesign-react/aigc'; const ChatActionBarExample = () => { const onActions = (name, data) => { diff --git a/packages/components/chat-actionbar/_example/custom.tsx b/packages/pro-components/chat/chat-actionbar/_example/custom.tsx similarity index 78% rename from packages/components/chat-actionbar/_example/custom.tsx rename to packages/pro-components/chat/chat-actionbar/_example/custom.tsx index f238548abb..c3197e8ffc 100644 --- a/packages/components/chat-actionbar/_example/custom.tsx +++ b/packages/pro-components/chat/chat-actionbar/_example/custom.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Space, ChatActionBar } from 'tdesign-react'; +import { Space } from 'tdesign-react'; +import { ChatActionBar } from '@tdesign-react/aigc'; const ChatActionBarExample = () => { const onActions = (name, data) => { diff --git a/packages/components/chat-actionbar/chat-actionbar.en-US.md b/packages/pro-components/chat/chat-actionbar/chat-actionbar.en-US.md similarity index 100% rename from packages/components/chat-actionbar/chat-actionbar.en-US.md rename to packages/pro-components/chat/chat-actionbar/chat-actionbar.en-US.md diff --git a/packages/components/chat-actionbar/chat-actionbar.md b/packages/pro-components/chat/chat-actionbar/chat-actionbar.md similarity index 100% rename from packages/components/chat-actionbar/chat-actionbar.md rename to packages/pro-components/chat/chat-actionbar/chat-actionbar.md diff --git a/packages/components/chat-actionbar/index.ts b/packages/pro-components/chat/chat-actionbar/index.ts similarity index 100% rename from packages/components/chat-actionbar/index.ts rename to packages/pro-components/chat/chat-actionbar/index.ts diff --git a/packages/components/chat-attachment/_example/base.tsx b/packages/pro-components/chat/chat-attachments/_example/base.tsx similarity index 91% rename from packages/components/chat-attachment/_example/base.tsx rename to packages/pro-components/chat/chat-attachments/_example/base.tsx index c76c45609c..d7de24e8c8 100644 --- a/packages/components/chat-attachment/_example/base.tsx +++ b/packages/pro-components/chat/chat-attachments/_example/base.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { ChatAttachments, TdAttachmentItem } from 'tdesign-react'; -import Space from '../../space/Space'; +import { ChatAttachments, TdAttachmentItem } from '@tdesign-react/aigc'; +import { Space } from 'tdesign-react'; const filesList: TdAttachmentItem[] = [ { diff --git a/packages/components/chat-attachment/_usage/index.jsx b/packages/pro-components/chat/chat-attachments/_usage/index.jsx similarity index 89% rename from packages/components/chat-attachment/_usage/index.jsx rename to packages/pro-components/chat/chat-attachments/_usage/index.jsx index a051eba4a8..3149511652 100644 --- a/packages/components/chat-attachment/_usage/index.jsx +++ b/packages/pro-components/chat/chat-attachments/_usage/index.jsx @@ -7,11 +7,10 @@ import React, { useState, useEffect, useMemo } from 'react'; import BaseUsage, { useConfigChange, usePanelChange } from '@tdesign/react-site/src/components/BaseUsage'; import jsxToString from 'react-element-to-jsx-string'; +import { ChatAttachments } from '@tdesign-react/aigc'; import configProps from './props.json'; -import { ChatAttachments } from 'tdesign-react'; - -const filesList= [ +const filesList = [ { name: 'excel-file.xlsx', size: 111111, @@ -70,7 +69,11 @@ export default function Usage() { const [renderComp, setRenderComp] = useState(); useEffect(() => { - setRenderComp(
); + setRenderComp( +
+ +
, + ); }, [changedProps]); const jsxStr = useMemo(() => { diff --git a/packages/components/chat-attachment/_usage/props.json b/packages/pro-components/chat/chat-attachments/_usage/props.json similarity index 100% rename from packages/components/chat-attachment/_usage/props.json rename to packages/pro-components/chat/chat-attachments/_usage/props.json diff --git a/packages/components/chat-attachment/chat-attachment.en-US.md b/packages/pro-components/chat/chat-attachments/chat-attachments.en-US.md similarity index 100% rename from packages/components/chat-attachment/chat-attachment.en-US.md rename to packages/pro-components/chat/chat-attachments/chat-attachments.en-US.md diff --git a/packages/components/chat-attachment/chat-attachment.md b/packages/pro-components/chat/chat-attachments/chat-attachments.md similarity index 99% rename from packages/components/chat-attachment/chat-attachment.md rename to packages/pro-components/chat/chat-attachments/chat-attachments.md index c78a3b2f86..dabb8c6a65 100644 --- a/packages/components/chat-attachment/chat-attachment.md +++ b/packages/pro-components/chat/chat-attachments/chat-attachments.md @@ -8,7 +8,7 @@ spline: aigc ## 基础用法 -{{ base }} + ## API diff --git a/packages/components/chat-attachment/index.ts b/packages/pro-components/chat/chat-attachments/index.ts similarity index 100% rename from packages/components/chat-attachment/index.ts rename to packages/pro-components/chat/chat-attachments/index.ts diff --git a/packages/components/chat-filecard/_example/base.tsx b/packages/pro-components/chat/chat-filecard/_example/base.tsx similarity index 91% rename from packages/components/chat-filecard/_example/base.tsx rename to packages/pro-components/chat/chat-filecard/_example/base.tsx index 6e7e1fbe88..c1aeec76bb 100644 --- a/packages/components/chat-filecard/_example/base.tsx +++ b/packages/pro-components/chat/chat-filecard/_example/base.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Filecard, Space, type TdAttachmentItem } from 'tdesign-react'; +import { Space } from 'tdesign-react'; +import { Filecard, type TdAttachmentItem } from '@tdesign-react/aigc'; const filesList: TdAttachmentItem[] = [ { diff --git a/packages/components/chat-filecard/chat-filecard.en-US.md b/packages/pro-components/chat/chat-filecard/chat-filecard.en-US.md similarity index 100% rename from packages/components/chat-filecard/chat-filecard.en-US.md rename to packages/pro-components/chat/chat-filecard/chat-filecard.en-US.md diff --git a/packages/components/chat-filecard/chat-filecard.md b/packages/pro-components/chat/chat-filecard/chat-filecard.md similarity index 100% rename from packages/components/chat-filecard/chat-filecard.md rename to packages/pro-components/chat/chat-filecard/chat-filecard.md diff --git a/packages/components/chat-filecard/index.ts b/packages/pro-components/chat/chat-filecard/index.ts similarity index 100% rename from packages/components/chat-filecard/index.ts rename to packages/pro-components/chat/chat-filecard/index.ts diff --git a/packages/components/chat-loading/_example/base.tsx b/packages/pro-components/chat/chat-loading/_example/base.tsx similarity index 84% rename from packages/components/chat-loading/_example/base.tsx rename to packages/pro-components/chat/chat-loading/_example/base.tsx index 09a9fc6cf8..2a8b00de2b 100644 --- a/packages/components/chat-loading/_example/base.tsx +++ b/packages/pro-components/chat/chat-loading/_example/base.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { ChatLoading, Space } from 'tdesign-react'; +import { Space } from 'tdesign-react'; +import { ChatLoading } from '@tdesign-react/aigc'; const ChatLoadingExample = () => ( <> diff --git a/packages/components/chat-loading/chat-loading.en-US.md b/packages/pro-components/chat/chat-loading/chat-loading.en-US.md similarity index 100% rename from packages/components/chat-loading/chat-loading.en-US.md rename to packages/pro-components/chat/chat-loading/chat-loading.en-US.md diff --git a/packages/components/chat-loading/chat-loading.md b/packages/pro-components/chat/chat-loading/chat-loading.md similarity index 100% rename from packages/components/chat-loading/chat-loading.md rename to packages/pro-components/chat/chat-loading/chat-loading.md diff --git a/packages/components/chat-loading/index.ts b/packages/pro-components/chat/chat-loading/index.ts similarity index 100% rename from packages/components/chat-loading/index.ts rename to packages/pro-components/chat/chat-loading/index.ts diff --git a/packages/components/chat-markdown/_example/base.tsx b/packages/pro-components/chat/chat-markdown/_example/base.tsx similarity index 96% rename from packages/components/chat-markdown/_example/base.tsx rename to packages/pro-components/chat/chat-markdown/_example/base.tsx index e4f29de619..c34eb95606 100644 --- a/packages/components/chat-markdown/_example/base.tsx +++ b/packages/pro-components/chat/chat-markdown/_example/base.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Button, ChatMarkdown } from 'tdesign-react'; -import Space from '../../space/Space'; +import { Button, Space } from 'tdesign-react'; +import { ChatMarkdown } from '@tdesign-react/aigc'; const doc = ` # This is TDesign diff --git a/packages/components/chat-markdown/_example/custom.tsx b/packages/pro-components/chat/chat-markdown/_example/custom.tsx similarity index 100% rename from packages/components/chat-markdown/_example/custom.tsx rename to packages/pro-components/chat/chat-markdown/_example/custom.tsx diff --git a/packages/components/chat-markdown/_example/mock.md b/packages/pro-components/chat/chat-markdown/_example/mock.md similarity index 100% rename from packages/components/chat-markdown/_example/mock.md rename to packages/pro-components/chat/chat-markdown/_example/mock.md diff --git a/packages/components/chat-markdown/_example/plugin.tsx b/packages/pro-components/chat/chat-markdown/_example/plugin.tsx similarity index 93% rename from packages/components/chat-markdown/_example/plugin.tsx rename to packages/pro-components/chat/chat-markdown/_example/plugin.tsx index c41d54201c..a3fe8a5792 100644 --- a/packages/components/chat-markdown/_example/plugin.tsx +++ b/packages/pro-components/chat/chat-markdown/_example/plugin.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; -import { Space, Switch, ChatMessage, ChatMarkdown } from 'tdesign-react'; +import { Space, Switch } from 'tdesign-react'; +import { ChatMarkdown } from '@tdesign-react/aigc'; import mdContent from './mock.md?raw'; const MarkdownExample = () => { diff --git a/packages/components/chat-markdown/chat-markdown.en-US.md b/packages/pro-components/chat/chat-markdown/chat-markdown.en-US.md similarity index 100% rename from packages/components/chat-markdown/chat-markdown.en-US.md rename to packages/pro-components/chat/chat-markdown/chat-markdown.en-US.md diff --git a/packages/components/chat-markdown/chat-markdown.md b/packages/pro-components/chat/chat-markdown/chat-markdown.md similarity index 100% rename from packages/components/chat-markdown/chat-markdown.md rename to packages/pro-components/chat/chat-markdown/chat-markdown.md diff --git a/packages/components/chat-markdown/index.ts b/packages/pro-components/chat/chat-markdown/index.ts similarity index 100% rename from packages/components/chat-markdown/index.ts rename to packages/pro-components/chat/chat-markdown/index.ts diff --git a/packages/components/chat-message/_example/action.tsx b/packages/pro-components/chat/chat-message/_example/action.tsx similarity index 90% rename from packages/components/chat-message/_example/action.tsx rename to packages/pro-components/chat/chat-message/_example/action.tsx index 016d298a0f..d0f1f114a7 100644 --- a/packages/components/chat-message/_example/action.tsx +++ b/packages/pro-components/chat/chat-message/_example/action.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { ChatActionBar, ChatMessage, Space } from 'tdesign-react'; +import { Space } from 'tdesign-react'; +import { ChatActionBar, ChatMessage } from '@tdesign-react/aigc'; + import { AIMessage, getMessageContentForCopy } from '@tencent/tdesign-chatbot'; const message: AIMessage = { diff --git a/packages/components/chat-message/_example/base.tsx b/packages/pro-components/chat/chat-message/_example/base.tsx similarity index 83% rename from packages/components/chat-message/_example/base.tsx rename to packages/pro-components/chat/chat-message/_example/base.tsx index c896dd890e..d38d818877 100644 --- a/packages/components/chat-message/_example/base.tsx +++ b/packages/pro-components/chat/chat-message/_example/base.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { ChatMessage, Space, UserMessage } from 'tdesign-react'; +import { Space } from 'tdesign-react'; +import { UserMessage, ChatMessage } from '@tdesign-react/aigc'; const message: UserMessage = { content: [ diff --git a/packages/components/chat-message/_example/configure.tsx b/packages/pro-components/chat/chat-message/_example/configure.tsx similarity index 93% rename from packages/components/chat-message/_example/configure.tsx rename to packages/pro-components/chat/chat-message/_example/configure.tsx index c4515581b6..4457762201 100644 --- a/packages/components/chat-message/_example/configure.tsx +++ b/packages/pro-components/chat/chat-message/_example/configure.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { AIMessage, ChatMessage, Divider, Space, SystemMessage, UserMessage } from 'tdesign-react'; +import { Divider, Space } from 'tdesign-react'; +import { AIMessage, ChatMessage, SystemMessage, UserMessage } from '@tdesign-react/aigc'; const messages = { ai: { diff --git a/packages/components/chat-message/_example/content.tsx b/packages/pro-components/chat/chat-message/_example/content.tsx similarity index 97% rename from packages/components/chat-message/_example/content.tsx rename to packages/pro-components/chat/chat-message/_example/content.tsx index d90b2f2aaa..39ad9c3f89 100644 --- a/packages/components/chat-message/_example/content.tsx +++ b/packages/pro-components/chat/chat-message/_example/content.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { AIMessage, ChatMessage, Space, UserMessage } from 'tdesign-react'; +import { Space } from 'tdesign-react'; + +import { AIMessage, ChatMessage, UserMessage } from '@tdesign-react/aigc'; const userMessage1: UserMessage = { id: '11111', diff --git a/packages/components/chat-message/_example/custom.tsx b/packages/pro-components/chat/chat-message/_example/custom.tsx similarity index 95% rename from packages/components/chat-message/_example/custom.tsx rename to packages/pro-components/chat/chat-message/_example/custom.tsx index dfc373f24f..b9e8be7aca 100644 --- a/packages/components/chat-message/_example/custom.tsx +++ b/packages/pro-components/chat/chat-message/_example/custom.tsx @@ -1,6 +1,8 @@ import React from 'react'; import TvisionTcharts from 'tvision-charts-react'; -import { BaseContent, ChatMessage, Space } from 'tdesign-react'; +import { Space } from 'tdesign-react'; + +import { BaseContent, ChatMessage } from '@tdesign-react/aigc'; // 扩展自定义消息体类型 declare module 'tdesign-react' { diff --git a/packages/components/chat-message/_example/status.tsx b/packages/pro-components/chat/chat-message/_example/status.tsx similarity index 90% rename from packages/components/chat-message/_example/status.tsx rename to packages/pro-components/chat/chat-message/_example/status.tsx index 8e2c28e40e..b316712a18 100644 --- a/packages/components/chat-message/_example/status.tsx +++ b/packages/pro-components/chat/chat-message/_example/status.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; -import { AIMessage, ChatMessage, Divider, Space, Select, TdChatLoadingProps } from 'tdesign-react'; +import { Divider, Space, Select } from 'tdesign-react'; +import { AIMessage, ChatMessage, TdChatLoadingProps } from '@tdesign-react/aigc'; const messages: Record = { loading: { diff --git a/packages/components/chat-message/chat-message.en-US.md b/packages/pro-components/chat/chat-message/chat-message.en-US.md similarity index 100% rename from packages/components/chat-message/chat-message.en-US.md rename to packages/pro-components/chat/chat-message/chat-message.en-US.md diff --git a/packages/components/chat-message/chat-message.md b/packages/pro-components/chat/chat-message/chat-message.md similarity index 100% rename from packages/components/chat-message/chat-message.md rename to packages/pro-components/chat/chat-message/chat-message.md diff --git a/packages/components/chat-message/index.ts b/packages/pro-components/chat/chat-message/index.ts similarity index 100% rename from packages/components/chat-message/index.ts rename to packages/pro-components/chat/chat-message/index.ts diff --git a/packages/components/chat-sender/_example/attachment.tsx b/packages/pro-components/chat/chat-sender/_example/attachment.tsx similarity index 93% rename from packages/components/chat-sender/_example/attachment.tsx rename to packages/pro-components/chat/chat-sender/_example/attachment.tsx index 43e1792303..338e8ee03d 100644 --- a/packages/components/chat-sender/_example/attachment.tsx +++ b/packages/pro-components/chat/chat-sender/_example/attachment.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; -import { ChatSender, TdAttachmentItem, UploadFile } from 'tdesign-react'; +import type { UploadFile } from 'tdesign-react'; +import { ChatSender, TdAttachmentItem } from '@tdesign-react/aigc'; const ChatSenderExample = () => { const [inputValue, setInputValue] = useState('输入内容'); @@ -45,7 +46,7 @@ const ChatSenderExample = () => { const onAttachmentsSelect = (e: CustomEvent) => { // 添加新文件并模拟上传进度 - let newFile = { + const newFile = { ...e.detail[0], name: e.detail[0].name, status: 'progress' as UploadFile['status'], diff --git a/packages/components/chat-sender/_example/base.tsx b/packages/pro-components/chat/chat-sender/_example/base.tsx similarity index 94% rename from packages/components/chat-sender/_example/base.tsx rename to packages/pro-components/chat/chat-sender/_example/base.tsx index 74ede45555..a229e60428 100644 --- a/packages/components/chat-sender/_example/base.tsx +++ b/packages/pro-components/chat/chat-sender/_example/base.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { ChatSender } from 'tdesign-react'; +import { ChatSender } from '@tdesign-react/aigc'; const ChatSenderExample = () => { const [inputValue, setInputValue] = useState('输入内容'); diff --git a/packages/components/chat-sender/_example/custom.tsx b/packages/pro-components/chat/chat-sender/_example/custom.tsx similarity index 97% rename from packages/components/chat-sender/_example/custom.tsx rename to packages/pro-components/chat/chat-sender/_example/custom.tsx index 31b8836112..53f01df826 100644 --- a/packages/components/chat-sender/_example/custom.tsx +++ b/packages/pro-components/chat/chat-sender/_example/custom.tsx @@ -1,6 +1,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { EnterIcon, InternetIcon, AttachIcon, CloseIcon, ArrowUpIcon, StopIcon } from 'tdesign-icons-react'; -import { ChatSender, Space, Button, Tag, Dropdown, Tooltip } from 'tdesign-react'; +import { ChatSender } from '@tdesign-react/aigc'; +import { Space, Button, Tag, Dropdown, Tooltip } from 'tdesign-react'; const options = [ { diff --git a/packages/components/chat-sender/_example/style.css b/packages/pro-components/chat/chat-sender/_example/style.css similarity index 100% rename from packages/components/chat-sender/_example/style.css rename to packages/pro-components/chat/chat-sender/_example/style.css diff --git a/packages/components/chat-sender/chat-sender.en-US.md b/packages/pro-components/chat/chat-sender/chat-sender.en-US.md similarity index 100% rename from packages/components/chat-sender/chat-sender.en-US.md rename to packages/pro-components/chat/chat-sender/chat-sender.en-US.md diff --git a/packages/components/chat-sender/chat-sender.md b/packages/pro-components/chat/chat-sender/chat-sender.md similarity index 100% rename from packages/components/chat-sender/chat-sender.md rename to packages/pro-components/chat/chat-sender/chat-sender.md diff --git a/packages/components/chat-sender/index.ts b/packages/pro-components/chat/chat-sender/index.ts similarity index 100% rename from packages/components/chat-sender/index.ts rename to packages/pro-components/chat/chat-sender/index.ts diff --git a/packages/components/chat-thinking/_example/base.tsx b/packages/pro-components/chat/chat-thinking/_example/base.tsx similarity index 97% rename from packages/components/chat-thinking/_example/base.tsx rename to packages/pro-components/chat/chat-thinking/_example/base.tsx index ff49e211bf..2c44631612 100644 --- a/packages/components/chat-thinking/_example/base.tsx +++ b/packages/pro-components/chat/chat-thinking/_example/base.tsx @@ -1,6 +1,6 @@ import { type MessageStatus } from '@tencent/tdesign-chatbot'; import React, { useState, useEffect, useRef } from 'react'; -import { ChatThinking } from 'tdesign-react'; +import { ChatThinking } from '@tdesign-react/aigc'; const fullText = '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。'; diff --git a/packages/components/chat-thinking/_example/style.tsx b/packages/pro-components/chat/chat-thinking/_example/style.tsx similarity index 92% rename from packages/components/chat-thinking/_example/style.tsx rename to packages/pro-components/chat/chat-thinking/_example/style.tsx index 2de373b866..102bd5cfa8 100644 --- a/packages/components/chat-thinking/_example/style.tsx +++ b/packages/pro-components/chat/chat-thinking/_example/style.tsx @@ -1,22 +1,12 @@ -import { TdChatThinkContentProps, type MessageStatus } from '@tencent/tdesign-chatbot'; import React, { useState, useEffect, useRef } from 'react'; -import { ChatThinking, Radio, RadioOption } from 'tdesign-react'; -import Space from '../../space/Space'; +import { Radio, Space } from 'tdesign-react'; +import { ChatThinking } from '@tdesign-react/aigc'; + +import type { TdChatThinkContentProps, MessageStatus } from '@tencent/tdesign-chatbot'; const fullText = '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。'; -const objOptions: RadioOption[] = [ - { - value: 'border', - label: 'border', - }, - { - value: 'block', - label: 'block', - }, -]; - export default function ThinkContentDemo() { const [displayText, setDisplayText] = useState(''); const [status, setStatus] = useState('pending'); diff --git a/packages/components/chat-thinking/chat-thinking.en-US.md b/packages/pro-components/chat/chat-thinking/chat-thinking.en-US.md similarity index 100% rename from packages/components/chat-thinking/chat-thinking.en-US.md rename to packages/pro-components/chat/chat-thinking/chat-thinking.en-US.md diff --git a/packages/components/chat-thinking/chat-thinking.md b/packages/pro-components/chat/chat-thinking/chat-thinking.md similarity index 100% rename from packages/components/chat-thinking/chat-thinking.md rename to packages/pro-components/chat/chat-thinking/chat-thinking.md diff --git a/packages/components/chat-thinking/index.ts b/packages/pro-components/chat/chat-thinking/index.ts similarity index 100% rename from packages/components/chat-thinking/index.ts rename to packages/pro-components/chat/chat-thinking/index.ts diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index a6b517644c..c355004808 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useRef } from 'react'; import { CopyIcon, EditIcon, SoundIcon } from 'tdesign-icons-react'; import type { SSEChunkData, @@ -8,8 +8,10 @@ import type { ChatServiceConfig, BaseContent, ChatMessagesData, -} from 'tdesign-react'; -import { Button, ChatBot, Space } from 'tdesign-react'; +} from '@tdesign-react/aigc'; +import { Button, Space } from 'tdesign-react'; +import { ChatBot } from '@tdesign-react/aigc'; + import TvisionTcharts from 'tvision-charts-react'; // 1、扩展自定义消息体类型 diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 43bb80bfb5..9203b64ef5 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -13,7 +13,7 @@ import { ChatActionBar, isAIMessage, useChat, -} from 'tdesign-react'; +} from '@tdesign-react/aigc'; import { getMessageContentForCopy, TdChatActionsName } from '@tencent/tdesign-chatbot'; export default function ComponentsBuild() { diff --git a/packages/pro-components/chat/chatbot/_example/image.tsx b/packages/pro-components/chat/chatbot/_example/image.tsx index 2d99b8ff32..266bf6579c 100644 --- a/packages/pro-components/chat/chatbot/_example/image.tsx +++ b/packages/pro-components/chat/chatbot/_example/image.tsx @@ -11,8 +11,10 @@ import type { UploadFile, ChatRequestParams, TdChatMessageConfig, -} from 'tdesign-react'; -import { Button, ChatBot, Dropdown, Space, Image, type TdChatbotApi, ImageViewer, Skeleton } from 'tdesign-react'; + TdChatbotApi, +} from '@tdesign-react/aigc'; +import { ImageViewer, Skeleton, ImageViewerProps, Button, Dropdown, Space, Image } from 'tdesign-react'; +import { ChatBot } from '@tdesign-react/aigc'; const RatioOptions = [ { @@ -71,8 +73,6 @@ const mockData: ChatMessagesData[] = [ }, ]; -import type { ImageViewerProps } from 'tdesign-react'; - // 自定义生图消息内容 const BasicImageViewer = ({ images }) => { if (images?.length === 0 || images?.every((img) => img === undefined)) { @@ -217,7 +217,7 @@ export default function chatSample() { // 文件上传 const onFileSelect = (e: CustomEvent) => { // 添加新文件并模拟上传进度 - let newFile = { + const newFile = { ...e.detail[0], name: e.detail[0].name, status: 'progress' as UploadFile['status'], diff --git a/packages/pro-components/chat/chatbot/_example/research.tsx b/packages/pro-components/chat/chatbot/_example/research.tsx index 97cd8c6849..262b1d0fff 100644 --- a/packages/pro-components/chat/chatbot/_example/research.tsx +++ b/packages/pro-components/chat/chatbot/_example/research.tsx @@ -8,9 +8,9 @@ import type { UploadFile, ChatRequestParams, TdChatMessageConfig, -} from 'tdesign-react'; -import { ChatBot, type TdChatbotApi } from 'tdesign-react'; - + TdChatbotApi, +} from '@tdesign-react/aigc'; +import { ChatBot } from '@tdesign-react/aigc'; // 默认初始化消息 const mockData: ChatMessagesData[] = [ { @@ -109,7 +109,7 @@ export default function chatSample() { // 文件上传 const onFileSelect = (e: CustomEvent) => { // 添加新文件并模拟上传进度 - let newFile = { + const newFile = { ...e.detail[0], name: e.detail[0].name, status: 'progress' as UploadFile['status'], diff --git a/packages/pro-components/chat/chatbot/_example/searchContent.tsx b/packages/pro-components/chat/chatbot/_example/searchContent.tsx index d907ba211a..216798a625 100644 --- a/packages/pro-components/chat/chatbot/_example/searchContent.tsx +++ b/packages/pro-components/chat/chatbot/_example/searchContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ChatSearchContent } from 'tdesign-react'; +import { ChatSearchContent } from '@tdesign-react/aigc'; export default function SearchContentSample() { return ( diff --git a/packages/pro-components/chat/chatbot/_example/suggestionContent.tsx b/packages/pro-components/chat/chatbot/_example/suggestionContent.tsx index 2003ec6478..ac3907586b 100644 --- a/packages/pro-components/chat/chatbot/_example/suggestionContent.tsx +++ b/packages/pro-components/chat/chatbot/_example/suggestionContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ChatSuggestionContent } from 'tdesign-react'; +import { ChatSuggestionContent } from '@tdesign-react/aigc'; export default function SuggestionSample() { return ( diff --git a/packages/pro-components/chat/index.ts b/packages/pro-components/chat/index.ts index adc627eee6..5de1c62c51 100644 --- a/packages/pro-components/chat/index.ts +++ b/packages/pro-components/chat/index.ts @@ -1 +1,9 @@ export * from './chatbot'; +export * from './chat-actionbar'; +export * from './chat-attachments'; +export * from './chat-filecard'; +export * from './chat-loading'; +export * from './chat-markdown'; +export * from './chat-message'; +export * from './chat-sender'; +export * from './chat-thinking'; diff --git a/packages/tdesign-react-aigc/site/site.config.mjs b/packages/tdesign-react-aigc/site/site.config.mjs index 3207fcb348..61a4585940 100644 --- a/packages/tdesign-react-aigc/site/site.config.mjs +++ b/packages/tdesign-react-aigc/site/site.config.mjs @@ -44,8 +44,8 @@ export const docs = [ ], }, { - title: 'AIGC', - titleEn: 'aigc', + title: '智能对话', + titleEn: 'ChatBot', type: 'component', children: [ { @@ -56,6 +56,71 @@ export const docs = [ component: () => import('@tdesign/pro-components-chat/chatbot/chatbot.md'), componentEn: () => import('@tdesign/pro-components-chat/chatbot/chatbot.en-US.md'), }, + { + title: 'ChatSender 对话输入', + titleEn: 'ChatSender', + name: 'chat-sender', + path: '/react-aigc/components/chat-sender', + component: () => import('@tdesign/pro-components-chat/chat-sender/chat-sender.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-sender/chat-sender.en-US.md'), + }, + { + title: 'ChatMessage 对话消息体', + titleEn: 'ChatMessage', + name: 'chat-message', + path: '/react-aigc/components/chat-message', + component: () => import('@tdesign/pro-components-chat/chat-message/chat-message.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-message/chat-message.en-US.md'), + }, + { + title: 'ChatActionBar 对话操作栏', + titleEn: 'ChatActionBar', + name: 'chat-actionbar', + path: '/react-aigc/components/chat-actionbar', + component: () => import('@tdesign/pro-components-chat/chat-actionbar/chat-actionbar.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-actionbar/chat-actionbar.en-US.md'), + }, + { + title: 'ChatMarkdown 消息内容', + titleEn: 'ChatMarkdown', + name: 'chat-markdown', + path: '/react-aigc/components/chat-markdown', + component: () => import('@tdesign/pro-components-chat/chat-markdown/chat-markdown.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-markdown/chat-markdown.en-US.md'), + }, + { + title: 'ChatThinking 思考过程', + titleEn: 'ChatThinking', + name: 'chat-thinking', + path: '/react-aigc/components/chat-thinking', + component: () => import('@tdesign/pro-components-chat/chat-thinking/chat-thinking.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-thinking/chat-thinking.en-US.md'), + }, + + { + title: 'ChatLoading 对话加载', + titleEn: 'ChatLoading', + name: 'chat-loading', + path: '/react-aigc/components/chat-loading', + component: () => import('@tdesign/pro-components-chat/chat-loading/chat-loading.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-loading/chat-loading.en-US.md'), + }, + { + title: 'FileCard 文件缩略卡片', + titleEn: 'FileCard', + name: 'filecard', + path: '/react-aigc/components/chat-filecard', + component: () => import('@tdesign/pro-components-chat/chat-filecard/chat-filecard.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-filecard/chat-filecard.en-US.md'), + }, + { + title: 'ChatAttachments 附件列表', + titleEn: 'ChatAttachments', + name: 'chat-attachment', + path: '/react-aigc/components/chat-attachments', + component: () => import('@tdesign/pro-components-chat/chat-attachments/chat-attachments.md'), + componentEn: () => import('@tdesign/pro-components-chat/chat-attachments/chat-attachments.en-US.md'), + }, ], }, ]; diff --git a/tsconfig.json b/tsconfig.json index cd1fdddc3c..40f3147383 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,9 @@ "tdesign-react/*": [ "packages/components/*" ], + "@tdesign-react/aigc": [ + "packages/pro-components/chat" + ], "@test/utils": [ "test/utils" ], From 501fbedad596d34b798643ef4257de9e754bfb43 Mon Sep 17 00:00:00 2001 From: Uyarn Date: Tue, 20 May 2025 00:01:37 +0800 Subject: [PATCH 067/228] chore: remove deprecated config --- packages/tdesign-react-aigc/site/package.json | 1 - packages/tdesign-react-aigc/site/pwaConfig.js | 25 ------------------- .../tdesign-react-aigc/site/vite.config.js | 4 +-- 3 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 packages/tdesign-react-aigc/site/pwaConfig.js diff --git a/packages/tdesign-react-aigc/site/package.json b/packages/tdesign-react-aigc/site/package.json index b8e1140019..8246d5b838 100644 --- a/packages/tdesign-react-aigc/site/package.json +++ b/packages/tdesign-react-aigc/site/package.json @@ -35,7 +35,6 @@ "typescript": "5.6.2", "vite": "^5.4.7", "vite-plugin-istanbul": "^6.0.2", - "vite-plugin-pwa": "^0.20.5", "vite-plugin-tdoc": "^2.0.4", "vitest": "^2.1.1", "workbox-precaching": "^7.0.0" diff --git a/packages/tdesign-react-aigc/site/pwaConfig.js b/packages/tdesign-react-aigc/site/pwaConfig.js deleted file mode 100644 index 5ea8e0d6cc..0000000000 --- a/packages/tdesign-react-aigc/site/pwaConfig.js +++ /dev/null @@ -1,25 +0,0 @@ -export default { - strategies: 'injectManifest', - includeAssets: ['favicon.svg', 'favicon.ico', 'apple-touch-icon.png'], - injectManifest: { - maximumFileSizeToCacheInBytes: 7000000, - }, - manifest: { - name: 'TDesign for React', - short_name: 'TDesign', - description: 'React UI Component', - theme_color: '#ffffff', - icons: [ - { - src: 'pwa-192x192.png', - sizes: '192x192', - type: 'image/png', - }, - { - src: 'pwa-512x512.png', - sizes: '512x512', - type: 'image/png', - }, - ], - }, -}; diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js index bd16e24e6f..b25eea0f15 100644 --- a/packages/tdesign-react-aigc/site/vite.config.js +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -1,8 +1,6 @@ import path from 'path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -import { VitePWA } from 'vite-plugin-pwa'; -import pwaConfig from './pwaConfig'; import tdocPlugin from './plugin-tdoc'; const publicPathMap = { @@ -52,5 +50,5 @@ export default ({ mode }) => test: { environment: 'jsdom', }, - plugins: [react(), tdocPlugin(), VitePWA(pwaConfig), disableTreeShakingPlugin(['style/'])], + plugins: [react(), tdocPlugin(), disableTreeShakingPlugin(['style/'])], }); From 0a09d818136de51bce25617de0a95feafdc183dc Mon Sep 17 00:00:00 2001 From: Uyarn Date: Tue, 20 May 2025 00:30:09 +0800 Subject: [PATCH 068/228] feat: build site --- .../build/tdesign-react-aigc/rollup.config.js | 150 ++++++++++++++++++ package.json | 1 + packages/tdesign-react-aigc/site/src/App.jsx | 9 +- packages/tdesign-react-aigc/site/src/pwa.js | 10 -- packages/tdesign-react-aigc/site/src/sw.js | 8 - .../tdesign-react-aigc/site/vite.config.js | 3 +- pnpm-workspace.yaml | 1 + 7 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 internal/build/tdesign-react-aigc/rollup.config.js delete mode 100644 packages/tdesign-react-aigc/site/src/pwa.js delete mode 100644 packages/tdesign-react-aigc/site/src/sw.js diff --git a/internal/build/tdesign-react-aigc/rollup.config.js b/internal/build/tdesign-react-aigc/rollup.config.js new file mode 100644 index 0000000000..37529ebba2 --- /dev/null +++ b/internal/build/tdesign-react-aigc/rollup.config.js @@ -0,0 +1,150 @@ +import url from '@rollup/plugin-url'; +import json from '@rollup/plugin-json'; +import babel from '@rollup/plugin-babel'; +import styles from 'rollup-plugin-styles'; +import esbuild from 'rollup-plugin-esbuild'; +import replace from '@rollup/plugin-replace'; +import { terser } from 'rollup-plugin-terser'; +import commonjs from '@rollup/plugin-commonjs'; +import { DEFAULT_EXTENSIONS } from '@babel/core'; +import multiInput from 'rollup-plugin-multi-input'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import staticImport from 'rollup-plugin-static-import'; +import ignoreImport from 'rollup-plugin-ignore-import'; +import { resolve } from 'path'; + +import pkg from '../../../packages/tdesign-react-aigc/package.json'; + +console.log(pkg.dependencies, 'pkg.dependencies'); +const name = 'tdesign'; +const externalDeps = Object.keys(pkg.dependencies || {}); +const externalPeerDeps = Object.keys(pkg.peerDependencies || {}); +const banner = `/** + * ${name} v${pkg.version} + * (c) ${new Date().getFullYear()} ${pkg.author} + * @license ${pkg.license} + */ +`; +const inputList = [ + 'packages/pro-components/chat/**/*.ts', + 'packages/pro-components/chat/**/*.tsx', + '!packages/pro-components/chat/**/_example', + '!packages/pro-components/chat/**/_example-js', + '!packages/pro-components/chat/**/*.d.ts', + '!packages/pro-components/chat/**/__tests__', + '!packages/pro-components/chat/**/_usage', +]; + +const getPlugins = ({ env, isProd = false, ignoreLess = true, extractMultiCss = false } = {}) => { + const plugins = [ + nodeResolve({ + extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'], + }), + commonjs(), + esbuild({ + include: /\.[jt]sx?$/, + target: 'esnext', + minify: false, + jsx: 'transform', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + tsconfig: resolve(__dirname, '../tsconfig.build.json'), + }), + babel({ + babelHelpers: 'runtime', + extensions: [...DEFAULT_EXTENSIONS, '.ts', '.tsx'], + }), + json(), + url(), + replace({ + preventAssignment: true, + values: { + __VERSION__: JSON.stringify(pkg.version), + }, + }), + ]; + + if (extractMultiCss) { + plugins.push( + staticImport({ + baseDir: 'packages/pro-components/chat', + include: ['packages/components/**/style/css.js'], + }), + ignoreImport({ + include: ['packages/pro-components/chat/*/style/*'], + body: 'import "./css.js";', + }), + ); + } else if (ignoreLess) { + plugins.push(ignoreImport({ extensions: ['*.less'] })); + } else { + plugins.push( + staticImport({ + baseDir: 'packages/pro-components/chat', + include: ['packages/pro-components/chat/**/style/index.js'], + }), + staticImport({ + baseDir: 'packages/common', + include: ['packages/common/style/web/**/*.less'], + }), + ignoreImport({ + include: ['packages/pro-components/chat/*/style/*'], + body: 'import "./style/index.js";', + }), + ); + } + + if (env) { + plugins.push( + replace({ + preventAssignment: true, + values: { + 'process.env.NODE_ENV': JSON.stringify(env), + }, + }), + ); + } + + if (isProd) { + plugins.push( + terser({ + output: { + /* eslint-disable */ + ascii_only: true, + /* eslint-enable */ + }, + }), + ); + } + + return plugins; +}; + +const cssConfig = { + input: ['packages/components/**/style/index.js'], + plugins: [multiInput({ relative: 'packages/components/' }), styles({ mode: 'extract' })], + output: { + banner, + dir: './packages/tdesign-react/es', + sourcemap: true, + assetFileNames: '[name].css', + }, +}; + +// 按需加载组件 带 css 样式 +const esConfig = { + input: inputList, + // 为了保留 style/css.js + treeshake: false, + external: externalDeps.concat(externalPeerDeps), + plugins: [multiInput({ relative: 'packages/pro-components/chat' })].concat(getPlugins({ extractMultiCss: true })), + output: { + banner, + dir: 'packages/@tdesign-react/aigc/es/', + format: 'esm', + sourcemap: true, + chunkFileNames: '_chunks/dep-[hash].js', + }, +}; + +export default [esConfig]; diff --git a/package.json b/package.json index 78d019160b..68153a6fc6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test:coverage": "vitest run --coverage", "prebuild": "rimraf packages/tdesign-react/es/* packages/tdesign-react/lib/* packages/tdesign-react/dist/* packages/tdesign-react/esm/* packages/tdesign-react/cjs/*", "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && node script/utils/bundle-override.js && pnpm run build:tsc", + "build:aigc": "cross-env NODE_ENV=production rollup -c internal/build/tdesign-react-aigc/rollup.config.js", "build:tsc": "run-p build:tsc-*", "build:tsc-es": "tsc -p ./tsconfig.build.json --outDir packages/tdesign-react/es/", "build:tsc-esm": "tsc -p ./tsconfig.build.json --outDir packages/tdesign-react/esm/", diff --git a/packages/tdesign-react-aigc/site/src/App.jsx b/packages/tdesign-react-aigc/site/src/App.jsx index ca4fc215db..caad7dcafc 100644 --- a/packages/tdesign-react-aigc/site/src/App.jsx +++ b/packages/tdesign-react-aigc/site/src/App.jsx @@ -2,10 +2,6 @@ import React, { useEffect, useRef, useState, lazy, Suspense } from 'react'; import { BrowserRouter, Routes, Navigate, Route, useLocation, useNavigate, Outlet } from 'react-router-dom'; import semver from 'semver'; import Loading from '@tdesign/components/loading'; -import ConfigProvider from '@tdesign/components/config-provider'; -import zhConfig from '@tdesign/components/locale/zh_CN'; -import enConfig from '@tdesign/components/locale/en_US'; -import { getLang } from 'tdesign-site-components'; import packageJson from '../../package.json'; import * as siteConfig from '../site.config'; @@ -53,7 +49,6 @@ function Components() { const tdDocSearch = useRef(); const [version] = useState(currentVersion); - const [globalConfig] = useState(() => (getLang() === 'en' ? enConfig : zhConfig)); function initHistoryVersions() { fetch(registryUrl) @@ -109,7 +104,7 @@ function Components() { }, [location]); return ( - + <> @@ -124,7 +119,7 @@ function Components() { - + ); } diff --git a/packages/tdesign-react-aigc/site/src/pwa.js b/packages/tdesign-react-aigc/site/src/pwa.js deleted file mode 100644 index 3c1442b732..0000000000 --- a/packages/tdesign-react-aigc/site/src/pwa.js +++ /dev/null @@ -1,10 +0,0 @@ -import { registerSW } from 'virtual:pwa-register'; - -const updateSW = registerSW({ - onNeedRefresh() { - console.log('onNeedRefresh'); - }, - onOfflineReady() { - console.log('onOfflineReady'); - }, -}); diff --git a/packages/tdesign-react-aigc/site/src/sw.js b/packages/tdesign-react-aigc/site/src/sw.js deleted file mode 100644 index 67150b319c..0000000000 --- a/packages/tdesign-react-aigc/site/src/sw.js +++ /dev/null @@ -1,8 +0,0 @@ -import { precacheAndRoute } from 'workbox-precaching' - -precacheAndRoute(self.__WB_MANIFEST) - -self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') - self.skipWaiting() -}); diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js index b25eea0f15..6b5014b7cb 100644 --- a/packages/tdesign-react-aigc/site/vite.config.js +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -26,7 +26,8 @@ export default ({ mode }) => resolve: { alias: { '@tdesign-react/aigc': path.resolve(__dirname, '../../pro-components/chat'), - 'tdesign-react': path.resolve(__dirname, '../../components'), + 'tdesign-react': path.resolve(__dirname, '../../tdesign-react'), + 'tdesign-react/es': path.resolve(__dirname, '../../tdesign-react'), }, }, build: { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6e89016176..a96a77d986 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'packages/**' + - 'internal/**' - 'test' From 6ea6dd6c93064e11e909fe1f4da6746df5873176 Mon Sep 17 00:00:00 2001 From: Uyarn Date: Tue, 20 May 2025 00:46:36 +0800 Subject: [PATCH 069/228] feat: build site --- packages/tdesign-react-aigc/site/index.html | 52 ++++++++++---------- packages/tdesign-react-aigc/site/src/App.jsx | 35 +------------ 2 files changed, 28 insertions(+), 59 deletions(-) diff --git a/packages/tdesign-react-aigc/site/index.html b/packages/tdesign-react-aigc/site/index.html index d32dc653a5..76ada15883 100644 --- a/packages/tdesign-react-aigc/site/index.html +++ b/packages/tdesign-react-aigc/site/index.html @@ -1,31 +1,33 @@ - - - - - TDesign Web React - - - - - - - - + - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/packages/tdesign-react-aigc/site/src/App.jsx b/packages/tdesign-react-aigc/site/src/App.jsx index caad7dcafc..8862beb4b7 100644 --- a/packages/tdesign-react-aigc/site/src/App.jsx +++ b/packages/tdesign-react-aigc/site/src/App.jsx @@ -45,29 +45,8 @@ function Components() { const tdHeaderRef = useRef(); const tdDocAsideRef = useRef(); const tdDocContentRef = useRef(); - const tdSelectRef = useRef(); const tdDocSearch = useRef(); - const [version] = useState(currentVersion); - - function initHistoryVersions() { - fetch(registryUrl) - .then((res) => res.json()) - .then((res) => { - const options = []; - const versions = filterVersions(Object.keys(res.versions)); - - versions.forEach((v) => { - const nums = v.split('.'); - if (nums[0] === '0' && nums[1] < 21) return false; - - options.unshift({ label: v, value: v.replace(/\./g, '_') }); - }); - - tdSelectRef.current.options = options.sort((a, b) => (semver.gt(a.label, b.label) ? -1 : 1)); - }); - } - useEffect(() => { tdHeaderRef.current.framework = 'react'; tdDocSearch.current.docsearchInfo = { indexName: 'tdesign_doc_react' }; @@ -84,18 +63,8 @@ function Components() { }); }; - tdSelectRef.current.onchange = ({ detail }) => { - const { value: version } = detail; - if (version === currentVersion) return; - - const historyUrl = `https://${version}-tdesign-react.surge.sh`; - window.open(historyUrl, '_blank'); - tdSelectRef.current.value = currentVersion; - }; - if (isDev) return; - initHistoryVersions(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -109,9 +78,7 @@ function Components() { - - - + From cb64198f94e769d20e01bc3ddb4dcfdc3a2b3867 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Tue, 20 May 2025 19:41:07 +0800 Subject: [PATCH 070/228] feat(chatbot): code demo refine --- package.json | 2 +- packages/components/chatbot/_example/code.tsx | 27 ++++++++++--------- packages/components/chatbot/chatbot.md | 4 +-- server/chat/data/code.js | 9 +++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 6893e469c5..130d35d337 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,6 @@ "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0", - "@tencent/tdesign-chatbot": "1.0.0-beta.55" + "@tencent/tdesign-chatbot": "1.0.0-beta.56" } } diff --git a/packages/components/chatbot/_example/code.tsx b/packages/components/chatbot/_example/code.tsx index 036369da4d..eb760419b3 100644 --- a/packages/components/chatbot/_example/code.tsx +++ b/packages/components/chatbot/_example/code.tsx @@ -65,21 +65,22 @@ const PreviewCard = ({ header, desc, loading, code }) => { size="medium" theme="normal" title={header} - loading={loading} style={{ margin: '14px 0' }} actions={ - -
- 复制代码 - - - 预览 - - + loading ? ( + desc + ) : ( + + + 复制代码 + + + 预览 + + + ) } - > - {desc} - + > ); }; @@ -204,7 +205,7 @@ export default function chatSample() { // 示例:代码运行结果预览 case 'preview': return ( -
+
Date: Tue, 20 May 2025 20:27:36 +0800 Subject: [PATCH 071/228] feat(chatbot): doc refine --- packages/pro-components/chat/chatbot/chatbot.md | 2 +- packages/tdesign-react-aigc/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index dc2e8c0d32..ea6c6a2160 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -39,7 +39,7 @@ spline: navigation {{ image }} ### 任务规划 -以下案例模拟了使用Chatbot搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,同时演示了**自定义任务流程渲染** +以下案例模拟了使用Chatbot搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,**自定义消息插槽名规则**,同时演示了**自定义任务流程渲染** {{ agent }} diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 0310376d3d..6eff8217fb 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -50,7 +50,7 @@ "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", - "@tencent/tdesign-chatbot": "1.0.0-beta.47", + "@tencent/tdesign-chatbot": "1.0.0-beta.57", "@types/sortablejs": "^1.10.7", "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.1.3", From 75ee260b916e5aca8c2c4572600da50dd7734532 Mon Sep 17 00:00:00 2001 From: Uyarn Date: Tue, 20 May 2025 20:48:13 +0800 Subject: [PATCH 072/228] chore: fix --- packages/tdesign-react-aigc/package.json | 1 + packages/tdesign-react-aigc/site/vite.config.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 6eff8217fb..8a7d28a4c4 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -65,6 +65,7 @@ "react-transition-group": "~4.4.1", "sortablejs": "^1.15.0", "tdesign-icons-react": "0.5.0", + "tdesign-react":"^1.12.1", "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0" diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js index 6b5014b7cb..713deafe9c 100644 --- a/packages/tdesign-react-aigc/site/vite.config.js +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -26,8 +26,6 @@ export default ({ mode }) => resolve: { alias: { '@tdesign-react/aigc': path.resolve(__dirname, '../../pro-components/chat'), - 'tdesign-react': path.resolve(__dirname, '../../tdesign-react'), - 'tdesign-react/es': path.resolve(__dirname, '../../tdesign-react'), }, }, build: { From 3fc7b05f48311be5e3b72aac86154272a4a98162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Tue, 20 May 2025 22:48:40 +0800 Subject: [PATCH 073/228] fix: use api request --- packages/pro-components/chat/chatbot/_example/agent.tsx | 2 +- packages/pro-components/chat/chatbot/_example/basic.tsx | 2 +- packages/pro-components/chat/chatbot/_example/code.tsx | 2 +- packages/pro-components/chat/chatbot/_example/custom.tsx | 2 +- packages/pro-components/chat/chatbot/_example/docs.tsx | 2 +- packages/pro-components/chat/chatbot/_example/hookComponent.tsx | 2 +- packages/pro-components/chat/chatbot/_example/image.tsx | 2 +- packages/pro-components/chat/chatbot/_example/research.tsx | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index 061bc5181d..da58098603 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -105,7 +105,7 @@ export default function ChatBotReact() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/agent', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/agent', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx index 2db6ce02d8..6723fab442 100644 --- a/packages/pro-components/chat/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -104,7 +104,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/code.tsx b/packages/pro-components/chat/chatbot/_example/code.tsx index aeb31d3557..a95be0e16f 100644 --- a/packages/pro-components/chat/chatbot/_example/code.tsx +++ b/packages/pro-components/chat/chatbot/_example/code.tsx @@ -134,7 +134,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index c355004808..0cf029cfcc 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -86,7 +86,7 @@ export default function ChatBotReact() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/docs.tsx b/packages/pro-components/chat/chatbot/_example/docs.tsx index 23edc28d14..87c66c74f9 100644 --- a/packages/pro-components/chat/chatbot/_example/docs.tsx +++ b/packages/pro-components/chat/chatbot/_example/docs.tsx @@ -57,7 +57,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 9203b64ef5..f06bcaac92 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -25,7 +25,7 @@ export default function ComponentsBuild() { // 聊天服务配置 chatServiceConfig: { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/image.tsx b/packages/pro-components/chat/chatbot/_example/image.tsx index 266bf6579c..fc6819dbbd 100644 --- a/packages/pro-components/chat/chatbot/_example/image.tsx +++ b/packages/pro-components/chat/chatbot/_example/image.tsx @@ -161,7 +161,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/research.tsx b/packages/pro-components/chat/chatbot/_example/research.tsx index 23edc28d14..87c66c74f9 100644 --- a/packages/pro-components/chat/chatbot/_example/research.tsx +++ b/packages/pro-components/chat/chatbot/_example/research.tsx @@ -57,7 +57,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { From 19207f1155df1378f861c7c9481bf5bd9031b6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Tue, 20 May 2025 22:48:40 +0800 Subject: [PATCH 074/228] fix: use api request --- .../chat/chatbot/_example/agent.tsx | 2 +- .../chat/chatbot/_example/basic.tsx | 2 +- .../chat/chatbot/_example/code.tsx | 2 +- .../chat/chatbot/_example/custom.tsx | 2 +- .../chat/chatbot/_example/docs.tsx | 2 +- .../chat/chatbot/_example/hookComponent.tsx | 2 +- .../chat/chatbot/_example/image.tsx | 2 +- .../chat/chatbot/_example/research.tsx | 2 +- server/chat/data/agent.js | 268 ---- server/chat/data/chart.js | 80 - server/chat/data/code.js | 593 -------- server/chat/data/docs.js | 283 ---- server/chat/data/image.js | 48 - server/chat/data/normal.js | 1293 ----------------- server/chat/ssemock.js | 203 --- 15 files changed, 8 insertions(+), 2776 deletions(-) delete mode 100644 server/chat/data/agent.js delete mode 100644 server/chat/data/chart.js delete mode 100644 server/chat/data/code.js delete mode 100644 server/chat/data/docs.js delete mode 100644 server/chat/data/image.js delete mode 100644 server/chat/data/normal.js delete mode 100644 server/chat/ssemock.js diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index 061bc5181d..da58098603 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -105,7 +105,7 @@ export default function ChatBotReact() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/agent', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/agent', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx index 2db6ce02d8..6723fab442 100644 --- a/packages/pro-components/chat/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -104,7 +104,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/code.tsx b/packages/pro-components/chat/chatbot/_example/code.tsx index aeb31d3557..a95be0e16f 100644 --- a/packages/pro-components/chat/chatbot/_example/code.tsx +++ b/packages/pro-components/chat/chatbot/_example/code.tsx @@ -134,7 +134,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index c355004808..0cf029cfcc 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -86,7 +86,7 @@ export default function ChatBotReact() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/docs.tsx b/packages/pro-components/chat/chatbot/_example/docs.tsx index 23edc28d14..87c66c74f9 100644 --- a/packages/pro-components/chat/chatbot/_example/docs.tsx +++ b/packages/pro-components/chat/chatbot/_example/docs.tsx @@ -57,7 +57,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 9203b64ef5..f06bcaac92 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -25,7 +25,7 @@ export default function ComponentsBuild() { // 聊天服务配置 chatServiceConfig: { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/image.tsx b/packages/pro-components/chat/chatbot/_example/image.tsx index 266bf6579c..fc6819dbbd 100644 --- a/packages/pro-components/chat/chatbot/_example/image.tsx +++ b/packages/pro-components/chat/chatbot/_example/image.tsx @@ -161,7 +161,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/research.tsx b/packages/pro-components/chat/chatbot/_example/research.tsx index 23edc28d14..87c66c74f9 100644 --- a/packages/pro-components/chat/chatbot/_example/research.tsx +++ b/packages/pro-components/chat/chatbot/_example/research.tsx @@ -57,7 +57,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'http://localhost:3000/sse/normal', + endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/server/chat/data/agent.js b/server/chat/data/agent.js deleted file mode 100644 index cdf427dfb8..0000000000 --- a/server/chat/data/agent.js +++ /dev/null @@ -1,268 +0,0 @@ -module.exports = [ - { type: 'text', msg: '为5岁' }, - { type: 'text', msg: '小朋友' }, - { type: 'text', msg: '准备' }, - { type: 'text', msg: '一场生日' }, - { type: 'text', msg: '派对,' }, - { type: 'text', msg: '我会' }, - { type: 'text', msg: '根据要求' }, - { type: 'text', msg: '准备' }, - { type: 'text', msg: '合适方案,' }, - { type: 'text', msg: '计划从' }, - { type: 'text', msg: '以下几个' }, - { type: 'text', msg: '步骤' }, - { type: 'text', msg: '进行准备:' }, - { - type: 'agent', - state: 'init', - id: 'task1', - content: { - text: '生日聚会规划任务已分解为3个执行阶段', - steps: [ - { step: '确定派对餐饮方案', agent_id: 'a1' }, - { step: '准备派对现场布置', agent_id: 'a2' }, - { step: '策划派对活动', agent_id: 'a3' }, - ], - }, - }, - { - type: 'agent', - state: 'command', - id: 'task1-a1-c1', - content: { - agent_id: 'a1', - text: '调用智能搜索工具,搜索儿童健康点心,儿童健康食谱', - }, - }, - { - type: 'agent', - state: 'command', - id: 'task1-a1-c2', - content: { - agent_id: 'a1', - text: '已筛选出3种高性价比菜单方案,开始进行营养匹配', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '推荐餐饮方案: ', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '主菜是香草烤鸡(无麸质),', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '准备耗时45分钟;', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '恐龙造型生日蛋糕,', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '可食用果蔬汁调色的面团;', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '水果蔬菜拼盘;', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a1-result', - content: { - agent_id: 'a1', - text: '饮品是鲜榨苹果汁,橙汁', - }, - }, - { - type: 'agent', - state: 'finish', - id: 'task1-a1-finish', - content: { - agent_id: 'a1', - }, - }, - { - type: 'agent', - state: 'command', - id: 'task1-a2-c1', - content: { - agent_id: 'a2', - text: '调用智能搜索工具,搜索儿童派对用品清单', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a2-result', - content: { - agent_id: 'a2', - text: '推荐现场布置方案:', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a2-result', - content: { - agent_id: 'a2', - text: '餐具(一次性纸盘、刀叉套装)', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a2-result', - content: { - agent_id: 'a2', - text: '、杯子、纸巾、一次性桌布,', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a2-result', - content: { - agent_id: 'a2', - text: '装饰气球、横幅、礼帽等,', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a2-result', - content: { - agent_id: 'a2', - text: '根据来访人数,可以选择零售渠道,价格从1-15元不等', - }, - }, - { - type: 'agent', - state: 'result', - id: 'task1-a2-result', - content: { - agent_id: 'a2', - text: ',让孩子参与布置过程,增加互动性', - }, - }, - { - type: 'agent', - state: 'finish', - id: 'task1-a2-finish', - content: { - agent_id: 'a2', - }, - }, - { - type: 'agent', - state: 'command', - id: 'task1-a3-c1', - content: { - agent_id: 'a3', - text: '搜索儿童派对游戏,安全、有趣、简单', - }, - }, - { - type: 'agent', - state: 'command', - id: 'task1-a3-c2', - content: { - agent_id: 'a3', - text: '整理信息并进行合理性分析,安全性评估', - }, - }, - { - type: 'agent', - state: 'result', - id: '888', - content: { - agent_id: 'a3', - text: '派对总时长建议控制在1.5小时,', - }, - }, - { - type: 'agent', - state: 'result', - id: '888', - content: { - agent_id: 'a3', - text: '符合5岁儿童注意力持续时间,', - }, - }, - { - type: 'agent', - state: 'result', - id: '888', - content: { - agent_id: 'a3', - text: '每位小朋友到达时可以在拍照区留影,', - }, - }, - { - type: 'agent', - state: 'result', - id: '888', - content: { - agent_id: 'a3', - text: '可设置一个签到板,', - }, - }, - { - type: 'agent', - state: 'result', - id: '888', - content: { - agent_id: 'a3', - text: '推荐活动:', - }, - }, - { - type: 'agent', - state: 'result', - id: '888', - content: { - agent_id: 'a3', - text: '尾巴追逐赛,彩泥制作,套圈,抽盲盒', - }, - }, - { - type: 'agent', - state: 'finish', - id: 'task1-a3-finish', - content: { - agent_id: 'a3', - }, - }, -]; diff --git a/server/chat/data/chart.js b/server/chat/data/chart.js deleted file mode 100644 index ee3105b183..0000000000 --- a/server/chat/data/chart.js +++ /dev/null @@ -1,80 +0,0 @@ -const chunks = [ - { type: 'text', msg: '今日' }, - { type: 'text', msg: '上午' }, - { type: 'text', msg: '北京道路' }, - { type: 'text', msg: '车辆' }, - { type: 'text', msg: '通行状况' }, - { type: 'text', msg: '9:00的峰值(1320),' }, - { type: 'text', msg: '可能显示早高峰拥堵最严重时段' }, - { type: 'text', msg: '10:00后缓慢回落,' }, - { type: 'text', msg: '可以得出如下折线图:' }, - { - type: 'chart', - data: { - id: 'c1', - chartType: 'line', - options: { - xAxis: { - type: 'category', - data: [ - '0:00', - '1:00', - '2:00', - '3:00', - '4:00', - '5:00', - '6:00', - '7:00', - '8:00', - '9:00', - '10:00', - '11:00', - '12:00', - ], - }, - yAxis: { - axisLabel: { inside: false }, - }, - series: [ - { - data: [500, 402, 382, 434, 560, 630, 720, 980, 1230, 1320, 1200, 1300, 1100], - type: 'line', - }, - ], - }, - }, - }, - { type: 'text', msg: '今日', paragraph: 'next' }, - { type: 'text', msg: '晚上' }, - { type: 'text', msg: '北京道路' }, - { type: 'text', msg: '车辆' }, - { type: 'text', msg: '通行状况' }, - { type: 'text', msg: '18:00的峰值(1322),' }, - { type: 'text', msg: '可能显示早高峰拥堵最严重时段' }, - { type: 'text', msg: '21:00后缓慢回落,' }, - { type: 'text', msg: '可以得出如下折线图:' }, - { - type: 'chart', - data: { - id: 'c2', - chartType: 'line', - options: { - xAxis: { - type: 'category', - data: ['16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '22:00', '23:00'], - }, - yAxis: { - axisLabel: { inside: false }, - }, - series: [ - { - data: [701, 921, 1322, 1091, 990, 810, 700, 500], - type: 'line', - }, - ], - }, - }, - }, -]; - -module.exports = chunks; diff --git a/server/chat/data/code.js b/server/chat/data/code.js deleted file mode 100644 index 1b1e6dfcd2..0000000000 --- a/server/chat/data/code.js +++ /dev/null @@ -1,593 +0,0 @@ -module.exports = [ - { type: 'text', msg: '下面是' }, - { type: 'text', msg: '一个使用' }, - { type: 'text', msg: 'TDesign' }, - { type: 'text', msg: '组件库' }, - - { type: 'text', msg: '实现' }, - - { type: 'text', msg: '的登' }, - - { type: 'text', msg: '录表单' }, - - { type: 'text', msg: '组件' }, - { type: 'text', msg: ',以React' }, - { type: 'text', msg: '版本' }, - { type: 'text', msg: '为例:' }, - { type: 'text', msg: '\n\n' }, - { - type: 'preview', - data: { - id: Date.now(), - enName: 'tdesign-login-form.jsx', - cnName: '正在生成中...', - version: 'v1', - }, - }, - { type: 'text', msg: '```', paragraph: 'next' }, - - { type: 'text', msg: 'js' }, - - { type: 'text', msg: 'x' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: 'import' }, - - { type: 'text', msg: ' {' }, - - { type: 'text', msg: ' Form' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: ' Input' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: ' Button' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: ' Message' }, - - { type: 'text', msg: ' }' }, - - { type: 'text', msg: ' from' }, - - { type: 'text', msg: " '" }, - - { type: 'text', msg: 'td' }, - - { type: 'text', msg: 'esign' }, - - { type: 'text', msg: '-react' }, - - { type: 'text', msg: "';\n\n" }, - - { type: 'text', msg: 'const' }, - - { type: 'text', msg: ' Login' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: ' =' }, - - { type: 'text', msg: ' ()' }, - - { type: 'text', msg: ' =\u003e' }, - - { type: 'text', msg: ' {\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' const' }, - - { type: 'text', msg: ' [' }, - - { type: 'text', msg: 'loading' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: ' set' }, - - { type: 'text', msg: 'Loading' }, - - { type: 'text', msg: ']' }, - - { type: 'text', msg: ' =' }, - - { type: 'text', msg: ' useState' }, - - { type: 'text', msg: '(false' }, - - { type: 'text', msg: ');\n\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' const' }, - - { type: 'text', msg: ' onSubmit' }, - - { type: 'text', msg: ' =' }, - - { type: 'text', msg: ' async' }, - - { type: 'text', msg: ' ({' }, - - { type: 'text', msg: ' validate' }, - - { type: 'text', msg: 'Result' }, - - { type: 'text', msg: ' })' }, - - { type: 'text', msg: ' =\u003e' }, - - { type: 'text', msg: ' {\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' if' }, - - { type: 'text', msg: ' (' }, - - { type: 'text', msg: 'validate' }, - - { type: 'text', msg: 'Result' }, - - { type: 'text', msg: ' ===' }, - - { type: 'text', msg: ' true' }, - - { type: 'text', msg: ')' }, - - { type: 'text', msg: ' {\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' set' }, - - { type: 'text', msg: 'Loading' }, - - { type: 'text', msg: '(true' }, - - { type: 'text', msg: ');\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' try' }, - - { type: 'text', msg: ' {\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' //' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '登录' }, - - { type: 'text', msg: '逻辑' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' Message' }, - - { type: 'text', msg: '.success' }, - - { type: 'text', msg: "('" }, - - { type: 'text', msg: '登录' }, - - { type: 'text', msg: '成功' }, - - { type: 'text', msg: "');\n" }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' }' }, - - { type: 'text', msg: ' catch' }, - - { type: 'text', msg: ' {\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' Message' }, - - { type: 'text', msg: '.error' }, - - { type: 'text', msg: "('" }, - - { type: 'text', msg: '登录' }, - - { type: 'text', msg: '失败' }, - - { type: 'text', msg: "');\n" }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' }' }, - - { type: 'text', msg: ' finally' }, - - { type: 'text', msg: ' {\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' set' }, - - { type: 'text', msg: 'Loading' }, - - { type: 'text', msg: '(false' }, - - { type: 'text', msg: ');\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' }\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' }\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' };\n\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' return' }, - - { type: 'text', msg: ' (\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: ' onSubmit' }, - - { type: 'text', msg: '={' }, - - { type: 'text', msg: 'on' }, - - { type: 'text', msg: 'Submit' }, - - { type: 'text', msg: '}\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '.Form' }, - - { type: 'text', msg: 'Item' }, - - { type: 'text', msg: ' name' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: 'username' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' label' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: '用户名' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' rules' }, - - { type: 'text', msg: '={' }, - - { type: 'text', msg: '[{' }, - - { type: 'text', msg: ' required' }, - - { type: 'text', msg: ':' }, - - { type: 'text', msg: ' true' }, - - { type: 'text', msg: ' }' }, - - { type: 'text', msg: ']' }, - - { type: 'text', msg: '}\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Input' }, - - { type: 'text', msg: ' placeholder' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: '请输入' }, - - { type: 'text', msg: '用户名' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' /\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c/' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '.Form' }, - - { type: 'text', msg: 'Item' }, - - { type: 'text', msg: '\u003e\n\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '.Form' }, - - { type: 'text', msg: 'Item' }, - - { type: 'text', msg: ' name' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: 'password' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' label' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: '密码' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' rules' }, - - { type: 'text', msg: '={' }, - - { type: 'text', msg: '[{' }, - - { type: 'text', msg: ' required' }, - - { type: 'text', msg: ':' }, - - { type: 'text', msg: ' true' }, - - { type: 'text', msg: ' }' }, - - { type: 'text', msg: ']' }, - - { type: 'text', msg: '}\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Input' }, - - { type: 'text', msg: ' type' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: 'password' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' /\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c/' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '.Form' }, - - { type: 'text', msg: 'Item' }, - - { type: 'text', msg: '\u003e\n\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '.Form' }, - - { type: 'text', msg: 'Item' }, - - { type: 'text', msg: '\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c' }, - - { type: 'text', msg: 'Button' }, - - { type: 'text', msg: ' theme' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: 'primary' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' type' }, - - { type: 'text', msg: '="' }, - - { type: 'text', msg: 'submit' }, - - { type: 'text', msg: '"' }, - - { type: 'text', msg: ' loading' }, - - { type: 'text', msg: '={' }, - - { type: 'text', msg: 'loading' }, - - { type: 'text', msg: '}' }, - - { type: 'text', msg: ' block' }, - - { type: 'text', msg: '\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '登录' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c/' }, - - { type: 'text', msg: 'Button' }, - - { type: 'text', msg: '\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c/' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '.Form' }, - - { type: 'text', msg: 'Item' }, - - { type: 'text', msg: '\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' \u003c/' }, - - { type: 'text', msg: 'Form' }, - - { type: 'text', msg: '\u003e\n' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: ' );\n' }, - - { type: 'text', msg: '};\n' }, - - { type: 'text', msg: '```\n\n' }, - { - type: 'preview', - data: { - id: Date.now(), - enName: 'tdesign-login-form.jsx', - cnName: '开始自动化测试...', - version: 'v1', - }, - }, - { type: 'text', msg: '这个', paragraph: 'next' }, - - { type: 'text', msg: '版本' }, - - { type: 'text', msg: '都' }, - - { type: 'text', msg: '包含了' }, - - { type: 'text', msg: ':\n' }, - - { type: 'text', msg: '1' }, - - { type: 'text', msg: '.' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '用户名' }, - - { type: 'text', msg: '和' }, - - { type: 'text', msg: '密码' }, - - { type: 'text', msg: '输入' }, - - { type: 'text', msg: '框' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '2' }, - - { type: 'text', msg: '.' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '必' }, - - { type: 'text', msg: '填' }, - - { type: 'text', msg: '验证' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '3' }, - - { type: 'text', msg: '.' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '加载' }, - - { type: 'text', msg: '状态' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '4' }, - - { type: 'text', msg: '.' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '基本的' }, - - { type: 'text', msg: '登录' }, - - { type: 'text', msg: '提交' }, - - { type: 'text', msg: '逻辑' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '5' }, - - { type: 'text', msg: '.' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '消息' }, - - { type: 'text', msg: '提示' }, - - { type: 'text', msg: '功能' }, - { - type: 'preview', - data: { - id: Date.now(), - enName: 'tdesign-login-form.jsx', - cnName: '生产完成', - version: 'v1', - }, - }, -]; diff --git a/server/chat/data/docs.js b/server/chat/data/docs.js deleted file mode 100644 index fdefb0009e..0000000000 --- a/server/chat/data/docs.js +++ /dev/null @@ -1,283 +0,0 @@ -module.exports = [ - { type: 'text', msg: '🌼' }, - - { type: 'text', msg: '宝' }, - - { type: 'text', msg: '子' }, - - { type: 'text', msg: '们' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '春天' }, - - { type: 'text', msg: '来' }, - - { type: 'text', msg: '啦' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '这些' }, - - { type: 'text', msg: '户外' }, - - { type: 'text', msg: '郊' }, - - { type: 'text', msg: '游' }, - - { type: 'text', msg: '打卡' }, - - { type: 'text', msg: '地' }, - - { type: 'text', msg: '你必须' }, - - { type: 'text', msg: '知道' }, - - { type: 'text', msg: '👏' }, - - { type: 'text', msg: '\n\n' }, - - { type: 'text', msg: '🌟' }, - - { type: 'text', msg: '郊' }, - - { type: 'text', msg: '野' }, - - { type: 'text', msg: '公园' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '这里' }, - - { type: 'text', msg: '有大' }, - - { type: 'text', msg: '片的' }, - - { type: 'text', msg: '草地' }, - - { type: 'text', msg: '和' }, - - { type: 'text', msg: '各种' }, - - { type: 'text', msg: '花卉' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '随便' }, - - { type: 'text', msg: '一' }, - - { type: 'text', msg: '拍' }, - - { type: 'text', msg: '都是' }, - - { type: 'text', msg: '大片' }, - - { type: 'text', msg: '既' }, - - { type: 'text', msg: '视' }, - - { type: 'text', msg: '感' }, - - { type: 'text', msg: '📷' }, - - { type: 'text', msg: '。' }, - - { type: 'text', msg: '还能' }, - - { type: 'text', msg: '放' }, - - { type: 'text', msg: '风筝' }, - - { type: 'text', msg: '、' }, - - { type: 'text', msg: '野' }, - - { type: 'text', msg: '餐' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '享受' }, - - { type: 'text', msg: '惬' }, - - { type: 'text', msg: '意的' }, - - { type: 'text', msg: '春' }, - - { type: 'text', msg: '日' }, - - { type: 'text', msg: '时光' }, - - { type: 'text', msg: '。\n\n' }, - - { type: 'text', msg: '🌳' }, - - { type: 'text', msg: '植物' }, - - { type: 'text', msg: '园' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '各种' }, - - { type: 'text', msg: '珍' }, - - { type: 'text', msg: '稀' }, - - { type: 'text', msg: '植物' }, - - { type: 'text', msg: '汇聚' }, - - { type: 'text', msg: '于此' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '仿佛' }, - - { type: 'text', msg: '置身' }, - - { type: 'text', msg: '于' }, - - { type: 'text', msg: '绿色的' }, - - { type: 'text', msg: '海洋' }, - - { type: 'text', msg: '。' }, - - { type: 'text', msg: '漫步' }, - - { type: 'text', msg: '其中' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '感受' }, - - { type: 'text', msg: '大' }, - - { type: 'text', msg: '自然的' }, - - { type: 'text', msg: '神奇' }, - - { type: 'text', msg: '与' }, - - { type: 'text', msg: '美丽' }, - - { type: 'text', msg: '。\n\n' }, - - { type: 'text', msg: '💧' }, - - { type: 'text', msg: '湖' }, - - { type: 'text', msg: '边' }, - - { type: 'text', msg: '湿地' }, - - { type: 'text', msg: '\n' }, - - { type: 'text', msg: '湖' }, - - { type: 'text', msg: '水' }, - - { type: 'text', msg: '清澈' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '周围' }, - - { type: 'text', msg: '生态环境' }, - - { type: 'text', msg: '优越' }, - - { type: 'text', msg: '。' }, - - { type: 'text', msg: '能看到' }, - - { type: 'text', msg: '很多' }, - - { type: 'text', msg: '候' }, - - { type: 'text', msg: '鸟' }, - - { type: 'text', msg: '和水' }, - - { type: 'text', msg: '生' }, - - { type: 'text', msg: '植物' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '是' }, - - { type: 'text', msg: '亲近' }, - - { type: 'text', msg: '自然' }, - - { type: 'text', msg: '的好' }, - - { type: 'text', msg: '去' }, - - { type: 'text', msg: '处' }, - - { type: 'text', msg: '。\n\n' }, - - { type: 'text', msg: '宝' }, - - { type: 'text', msg: '子' }, - - { type: 'text', msg: '们' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '赶紧' }, - - { type: 'text', msg: '收拾' }, - - { type: 'text', msg: '行' }, - - { type: 'text', msg: '囊' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '去' }, - - { type: 'text', msg: '这些' }, - - { type: 'text', msg: '地方' }, - - { type: 'text', msg: '打卡' }, - - { type: 'text', msg: '吧' }, - - { type: 'text', msg: '😜' }, - - { type: 'text', msg: '。\n\n' }, - - { type: 'text', msg: '#' }, - - { type: 'text', msg: '春天' }, - - { type: 'text', msg: '郊' }, - - { type: 'text', msg: '游' }, - - { type: 'text', msg: ' #' }, - - { type: 'text', msg: '打卡' }, - - { type: 'text', msg: '目的地' }, - - { type: 'text', msg: ' #' }, - - { type: 'text', msg: '户外' }, - - { type: 'text', msg: '之旅' }, - - { type: 'text', msg: ' #' }, - - { type: 'text', msg: '春' }, - - { type: 'text', msg: '日' }, - - { type: 'text', msg: '美景' }, -]; diff --git a/server/chat/data/image.js b/server/chat/data/image.js deleted file mode 100644 index 6a5d7b7b02..0000000000 --- a/server/chat/data/image.js +++ /dev/null @@ -1,48 +0,0 @@ -module.exports = [ - { type: 'text', msg: '接下来我将生成符合要求的图片' }, - { - type: 'image', - content: '[{"progress": 0},{"progress": 0}, {"progress": 0},{"progress": 0}]', - }, - { - type: 'image', - content: '[{"progress": 10},{"progress": 10}, {"progress": 10},{"progress": 10}]', - }, - { - type: 'image', - content: '[{"progress": 20},{"progress": 20}, {"progress": 20},{"progress": 20}]', - }, - { - type: 'image', - content: '[{"progress": 30},{"progress": 30}, {"progress": 30},{"progress": 30}]', - }, - { - type: 'image', - content: '[{"progress": 40},{"progress": 40}, {"progress": 40},{"progress": 40}]', - }, - { - type: 'image', - content: '[{"progress": 50},{"progress": 50}, {"progress": 50},{"progress": 50}]', - }, - { - type: 'image', - content: '[{"progress": 60},{"progress": 60}, {"progress": 60},{"progress": 60}]', - }, - { - type: 'image', - content: '[{"progress": 70},{"progress": 70}, {"progress": 70},{"progress": 70}]', - }, - { - type: 'image', - content: '[{"progress": 80},{"progress": 80}, {"progress": 80},{"progress": 80}]', - }, - { - type: 'image', - content: '[{"progress": 90},{"progress": 90}, {"progress": 90},{"progress": 90}]', - }, - { - type: 'image', - content: - '[{"url":"https://tdesign.gtimg.com/demo/demo-image-1.png","format":"png","width":1204,"height":1024,"size":1032},{"url":"https://tdesign.gtimg.com/demo/demo-image-2.png","format":"png","width":1204,"height":1024,"size":1032},{"url":"https://tdesign.gtimg.com/demo/demo-image-3.png","format":"png","width":1204,"height":1024,"size":1032}]', - }, -]; diff --git a/server/chat/data/normal.js b/server/chat/data/normal.js deleted file mode 100644 index bb76d94e12..0000000000 --- a/server/chat/data/normal.js +++ /dev/null @@ -1,1293 +0,0 @@ -const chunks = [ - { type: 'search', title: '开始获取资料...' }, - { - type: 'search', - title: '找到 2 篇相关资料', - content: [ - { title: '看看这些疯狂的自动取款机,千岛群岛存取款全靠海上移动ATM', url: 'https://example.com/ref1' }, - { title: '关于南极的10条冷知识,这里除了企鹅外,还有自动取款机!_百度文库', url: 'https://example.com/ref2' }, - ], - }, - { type: 'think', title: '思考中...', content: '嗯' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '用户' }, - - { type: 'think', title: '思考中...', content: '问' }, - - { type: 'think', title: '思考中...', content: '的是' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '的' }, - - { type: 'think', title: '思考中...', content: '自动' }, - - { type: 'think', title: '思考中...', content: '提' }, - - { type: 'think', title: '思考中...', content: '款' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: '叫什么' }, - - { type: 'think', title: '思考中...', content: '名字' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '这个问题' }, - - { type: 'think', title: '思考中...', content: '有点' }, - - { type: 'think', title: '思考中...', content: '有趣' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '因为' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '是一个' }, - - { type: 'think', title: '思考中...', content: '极端' }, - - { type: 'think', title: '思考中...', content: '寒冷' }, - - { type: 'think', title: '思考中...', content: '的地方' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '而且' }, - - { type: 'think', title: '思考中...', content: '大部分' }, - - { type: 'think', title: '思考中...', content: '地区' }, - - { type: 'think', title: '思考中...', content: '都是' }, - - { type: 'think', title: '思考中...', content: '无人' }, - - { type: 'think', title: '思考中...', content: '居住' }, - - { type: 'think', title: '思考中...', content: '的' }, - - { type: 'think', title: '思考中...', content: '科研' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '首先' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '我需要' }, - - { type: 'think', title: '思考中...', content: '确认' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '是否有' }, - - { type: 'think', title: '思考中...', content: '自动' }, - - { type: 'think', title: '思考中...', content: '提' }, - - { type: 'think', title: '思考中...', content: '款' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: '存在' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '\n\n' }, - - { type: 'think', title: '思考中...', content: '首先' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '大陆' }, - - { type: 'think', title: '思考中...', content: '没有' }, - - { type: 'think', title: '思考中...', content: '常住' }, - - { type: 'think', title: '思考中...', content: '居民' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '只有' }, - - { type: 'think', title: '思考中...', content: '各国的' }, - - { type: 'think', title: '思考中...', content: '科研' }, - - { type: 'think', title: '思考中...', content: '人员' }, - - { type: 'think', title: '思考中...', content: '季节性' }, - - { type: 'think', title: '思考中...', content: '驻扎' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '主要的' }, - - { type: 'think', title: '思考中...', content: '设施' }, - - { type: 'think', title: '思考中...', content: '是' }, - - { type: 'think', title: '思考中...', content: '各国的' }, - - { type: 'think', title: '思考中...', content: '科学' }, - - { type: 'think', title: '思考中...', content: '考察' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '美国的' }, - - { type: 'think', title: '思考中...', content: '麦克' }, - - { type: 'think', title: '思考中...', content: '默' }, - - { type: 'think', title: '思考中...', content: '多' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: '、' }, - - { type: 'think', title: '思考中...', content: '中国的' }, - - { type: 'think', title: '思考中...', content: '长城' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: '等' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '这些' }, - - { type: 'think', title: '思考中...', content: '站点' }, - - { type: 'think', title: '思考中...', content: '通常' }, - - { type: 'think', title: '思考中...', content: '有' }, - - { type: 'think', title: '思考中...', content: '基本' }, - - { type: 'think', title: '思考中...', content: '的生活' }, - - { type: 'think', title: '思考中...', content: '设施' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '但' }, - - { type: 'think', title: '思考中...', content: '像' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '这样的' }, - - { type: 'think', title: '思考中...', content: '商业' }, - - { type: 'think', title: '思考中...', content: '服务' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '非常' }, - - { type: 'think', title: '思考中...', content: '有限' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '接下来' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '我需要' }, - - { type: 'think', title: '思考中...', content: '查找' }, - - { type: 'think', title: '思考中...', content: '是否有' }, - - { type: 'think', title: '思考中...', content: '关于' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '自动' }, - - { type: 'think', title: '思考中...', content: '提' }, - - { type: 'think', title: '思考中...', content: '款' }, - - { type: 'think', title: '思考中...', content: '机的' }, - - { type: 'think', title: '思考中...', content: '信息' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '记得' }, - - { type: 'think', title: '思考中...', content: '之前' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '听说过' }, - - { type: 'think', title: '思考中...', content: '某个' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '在南' }, - - { type: 'think', title: '思考中...', content: '极' }, - - { type: 'think', title: '思考中...', content: '设立' }, - - { type: 'think', title: '思考中...', content: '过' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: '作为' }, - - { type: 'think', title: '思考中...', content: '宣传' }, - - { type: 'think', title: '思考中...', content: '噱' }, - - { type: 'think', title: '思考中...', content: '头' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '但' }, - - { type: 'think', title: '思考中...', content: '不确定' }, - - { type: 'think', title: '思考中...', content: '具体' }, - - { type: 'think', title: '思考中...', content: '细节' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '是一些' }, - - { type: 'think', title: '思考中...', content: '国际' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '为了' }, - - { type: 'think', title: '思考中...', content: '品牌' }, - - { type: 'think', title: '思考中...', content: '宣传' }, - - { type: 'think', title: '思考中...', content: '或者' }, - - { type: 'think', title: '思考中...', content: '方便' }, - - { type: 'think', title: '思考中...', content: '科研' }, - - { type: 'think', title: '思考中...', content: '人员' }, - - { type: 'think', title: '思考中...', content: '而' }, - - { type: 'think', title: '思考中...', content: '设置的' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '然后' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '我' }, - - { type: 'think', title: '思考中...', content: '想到' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '是在' }, - - { type: 'think', title: '思考中...', content: '麦克' }, - - { type: 'think', title: '思考中...', content: '默' }, - - { type: 'think', title: '思考中...', content: '多' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '那里' }, - - { type: 'think', title: '思考中...', content: '是' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '最大的' }, - - { type: 'think', title: '思考中...', content: '科研' }, - - { type: 'think', title: '思考中...', content: '社区' }, - - { type: 'think', title: '思考中...', content: '之一' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '由' }, - - { type: 'think', title: '思考中...', content: '美国' }, - - { type: 'think', title: '思考中...', content: '运作' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '那里' }, - - { type: 'think', title: '思考中...', content: '可能有' }, - - { type: 'think', title: '思考中...', content: '较多的' }, - - { type: 'think', title: '思考中...', content: '基础设施' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '包括' }, - - { type: 'think', title: '思考中...', content: '可能的' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '如果有' }, - - { type: 'think', title: '思考中...', content: '的话' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '可能是' }, - - { type: 'think', title: '思考中...', content: '由' }, - - { type: 'think', title: '思考中...', content: '某个' }, - - { type: 'think', title: '思考中...', content: '美国' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '设置的' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '富' }, - - { type: 'think', title: '思考中...', content: '兰' }, - - { type: 'think', title: '思考中...', content: '克林' }, - - { type: 'think', title: '思考中...', content: '国家' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '或者是' }, - - { type: 'think', title: '思考中...', content: '新西兰' }, - - { type: 'think', title: '思考中...', content: '的' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '因为' }, - - { type: 'think', title: '思考中...', content: '麦克' }, - - { type: 'think', title: '思考中...', content: '默' }, - - { type: 'think', title: '思考中...', content: '多' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: '靠近' }, - - { type: 'think', title: '思考中...', content: '新西兰' }, - - { type: 'think', title: '思考中...', content: '的' }, - - { type: 'think', title: '思考中...', content: '管辖' }, - - { type: 'think', title: '思考中...', content: '区域' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '另外' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '需要考虑' }, - - { type: 'think', title: '思考中...', content: '时间' }, - - { type: 'think', title: '思考中...', content: '因素' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '机的' }, - - { type: 'think', title: '思考中...', content: '安装' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '是在' }, - - { type: 'think', title: '思考中...', content: '某个' }, - - { type: 'think', title: '思考中...', content: '特定' }, - - { type: 'think', title: '思考中...', content: '时期' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '早期的' }, - - { type: 'think', title: '思考中...', content: '探险' }, - - { type: 'think', title: '思考中...', content: '活动' }, - - { type: 'think', title: '思考中...', content: '或者' }, - - { type: 'think', title: '思考中...', content: '近' }, - - { type: 'think', title: '思考中...', content: '年的' }, - - { type: 'think', title: '思考中...', content: '项目' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '需要' }, - - { type: 'think', title: '思考中...', content: '确认' }, - - { type: 'think', title: '思考中...', content: '这些' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '是否' }, - - { type: 'think', title: '思考中...', content: '仍然' }, - - { type: 'think', title: '思考中...', content: '存在' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '或者' }, - - { type: 'think', title: '思考中...', content: '是否' }, - - { type: 'think', title: '思考中...', content: '已经被' }, - - { type: 'think', title: '思考中...', content: '移除' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '还需要' }, - - { type: 'think', title: '思考中...', content: '考虑' }, - - { type: 'think', title: '思考中...', content: '是否有' }, - - { type: 'think', title: '思考中...', content: '相关的' }, - - { type: 'think', title: '思考中...', content: '新闻报道' }, - - { type: 'think', title: '思考中...', content: '或' }, - - { type: 'think', title: '思考中...', content: '官方' }, - - { type: 'think', title: '思考中...', content: '信息' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '例如' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '是否有' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '发布' }, - - { type: 'think', title: '思考中...', content: '过' }, - - { type: 'think', title: '思考中...', content: '在南' }, - - { type: 'think', title: '思考中...', content: '极' }, - - { type: 'think', title: '思考中...', content: '设立' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '的消息' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '或者' }, - - { type: 'think', title: '思考中...', content: '科研' }, - - { type: 'think', title: '思考中...', content: '站的' }, - - { type: 'think', title: '思考中...', content: '官方网站' }, - - { type: 'think', title: '思考中...', content: '提到' }, - - { type: 'think', title: '思考中...', content: '过' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '的存在' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '还需要' }, - - { type: 'think', title: '思考中...', content: '区分' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '洲' }, - - { type: 'think', title: '思考中...', content: '本身' }, - - { type: 'think', title: '思考中...', content: '是否有' }, - - { type: 'think', title: '思考中...', content: '独立的' }, - - { type: 'think', title: '思考中...', content: '金融机构' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '但' }, - - { type: 'think', title: '思考中...', content: '考虑到' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '条约' }, - - { type: 'think', title: '思考中...', content: '体系' }, - - { type: 'think', title: '思考中...', content: '规定' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: '用于' }, - - { type: 'think', title: '思考中...', content: '和平' }, - - { type: 'think', title: '思考中...', content: '与' }, - - { type: 'think', title: '思考中...', content: '科研' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '商业' }, - - { type: 'think', title: '思考中...', content: '活动' }, - - { type: 'think', title: '思考中...', content: '有限' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '因此' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '没有' }, - - { type: 'think', title: '思考中...', content: '常' }, - - { type: 'think', title: '思考中...', content: '设' }, - - { type: 'think', title: '思考中...', content: '的' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '机构' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: '可能是' }, - - { type: 'think', title: '思考中...', content: '某个' }, - - { type: 'think', title: '思考中...', content: '国家' }, - - { type: 'think', title: '思考中...', content: '为了' }, - - { type: 'think', title: '思考中...', content: '本国' }, - - { type: 'think', title: '思考中...', content: '科' }, - - { type: 'think', title: '思考中...', content: '考' }, - - { type: 'think', title: '思考中...', content: '人员' }, - - { type: 'think', title: '思考中...', content: '方便' }, - - { type: 'think', title: '思考中...', content: '而' }, - - { type: 'think', title: '思考中...', content: '设立的' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '美国的' }, - - { type: 'think', title: '思考中...', content: '科' }, - - { type: 'think', title: '思考中...', content: '考' }, - - { type: 'think', title: '思考中...', content: '站' }, - - { type: 'think', title: '思考中...', content: '使用' }, - - { type: 'think', title: '思考中...', content: '美国的' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '由' }, - - { type: 'think', title: '思考中...', content: '某个' }, - - { type: 'think', title: '思考中...', content: '美国' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '运营' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '然后' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '涉及到' }, - - { type: 'think', title: '思考中...', content: '具体' }, - - { type: 'think', title: '思考中...', content: '名称' }, - - { type: 'think', title: '思考中...', content: '的问题' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '如果' }, - - { type: 'think', title: '思考中...', content: '确实' }, - - { type: 'think', title: '思考中...', content: '存在' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '机' }, - - { type: 'think', title: '思考中...', content: '属于' }, - - { type: 'think', title: '思考中...', content: '某个' }, - - { type: 'think', title: '思考中...', content: '特定的' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '富' }, - - { type: 'think', title: '思考中...', content: '兰' }, - - { type: 'think', title: '思考中...', content: '克林' }, - - { type: 'think', title: '思考中...', content: '国家' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '但' }, - - { type: 'think', title: '思考中...', content: '需要' }, - - { type: 'think', title: '思考中...', content: '查' }, - - { type: 'think', title: '思考中...', content: '证' }, - - { type: 'think', title: '思考中...', content: '。' }, - - { type: 'think', title: '思考中...', content: '或者' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '可能' }, - - { type: 'think', title: '思考中...', content: '有一个' }, - - { type: 'think', title: '思考中...', content: '特定的' }, - - { type: 'think', title: '思考中...', content: '昵' }, - - { type: 'think', title: '思考中...', content: '称' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '“' }, - - { type: 'think', title: '思考中...', content: '南极' }, - - { type: 'think', title: '思考中...', content: 'ATM' }, - - { type: 'think', title: '思考中...', content: '”' }, - - { type: 'think', title: '思考中...', content: '之类的' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '但' }, - - { type: 'think', title: '思考中...', content: '更' }, - - { type: 'think', title: '思考中...', content: '可能是' }, - - { type: 'think', title: '思考中...', content: '以' }, - - { type: 'think', title: '思考中...', content: '所属' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '命名' }, - - { type: 'think', title: '思考中...', content: '。\n\n' }, - - { type: 'think', title: '思考中...', content: '最后' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '可能需要' }, - - { type: 'think', title: '思考中...', content: '综合' }, - - { type: 'think', title: '思考中...', content: '这些' }, - - { type: 'think', title: '思考中...', content: '信息' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '给出' }, - - { type: 'think', title: '思考中...', content: '一个' }, - - { type: 'think', title: '思考中...', content: '准确的' }, - - { type: 'think', title: '思考中...', content: '答案' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '并' }, - - { type: 'think', title: '思考中...', content: '指出' }, - - { type: 'think', title: '思考中...', content: '可能的' }, - - { type: 'think', title: '思考中...', content: '背景' }, - - { type: 'think', title: '思考中...', content: ',' }, - - { type: 'think', title: '思考中...', content: '比如' }, - - { type: 'think', title: '思考中...', content: '安装' }, - - { type: 'think', title: '思考中...', content: '时间' }, - - { type: 'think', title: '思考中...', content: '、' }, - - { type: 'think', title: '思考中...', content: '所属' }, - - { type: 'think', title: '思考中...', content: '银行' }, - - { type: 'think', title: '思考中...', content: '以及' }, - - { type: 'think', title: '思考中...', content: '现状' }, - - { type: 'think', title: '思考中...', content: '如何' }, - - { type: 'think', title: '思考中...', content: '。\n' }, - - { type: 'think', title: '思考完成 (耗时17秒)', content: '' }, - - { type: 'text', msg: '\n\n' }, - - { type: 'text', msg: '南极' }, - - { type: 'text', msg: '的' }, - - { type: 'text', msg: '自动' }, - - { type: 'text', msg: '提' }, - - { type: 'text', msg: '款' }, - - { type: 'text', msg: '机' }, - - { type: 'text', msg: '并没有' }, - - { type: 'text', msg: '一个' }, - - { type: 'text', msg: '特定的' }, - - { type: 'text', msg: '专属' }, - - { type: 'text', msg: '名称' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '但' }, - - { type: 'text', msg: '历史上' }, - - { type: 'text', msg: '确实' }, - - { type: 'text', msg: '有一' }, - - { type: 'text', msg: '台' }, - - { type: 'text', msg: 'ATM' }, - - { type: 'text', msg: '机' }, - - { type: 'text', msg: '曾' }, - - { type: 'text', msg: '短暂' }, - - { type: 'text', msg: '存在于' }, - - { type: 'text', msg: '南极' }, - - { type: 'text', msg: '的' }, - - { type: 'text', msg: '**' }, - - { type: 'text', msg: '麦克' }, - - { type: 'text', msg: '默' }, - - { type: 'text', msg: '多' }, - - { type: 'text', msg: '站' }, - - { type: 'text', msg: '**' }, - - { type: 'text', msg: '(' }, - - { type: 'text', msg: 'Mc' }, - - { type: 'text', msg: 'Mur' }, - - { type: 'text', msg: 'do' }, - - { type: 'text', msg: ' Station' }, - - { type: 'text', msg: ')。' }, - - { type: 'text', msg: '这台' }, - - { type: 'text', msg: 'ATM' }, - - { type: 'text', msg: '由' }, - - { type: 'text', msg: '美国' }, - - { type: 'text', msg: '**' }, - - { type: 'text', msg: '富' }, - - { type: 'text', msg: '兰' }, - - { type: 'text', msg: '克林' }, - - { type: 'text', msg: '国家' }, - - { type: 'text', msg: '银行' }, - - { type: 'text', msg: '**' }, - - { type: 'text', msg: '(' }, - - { type: 'text', msg: 'W' }, - - { type: 'text', msg: 'ells' }, - - { type: 'text', msg: ' Fargo' }, - - { type: 'text', msg: ')' }, - - { type: 'text', msg: '于' }, - - { type: 'text', msg: '199' }, - - { type: 'text', msg: '8' }, - - { type: 'text', msg: '年' }, - - { type: 'text', msg: '安装' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '主要' }, - - { type: 'text', msg: '供' }, - - { type: 'text', msg: '驻扎' }, - - { type: 'text', msg: '在该' }, - - { type: 'text', msg: '站的' }, - - { type: 'text', msg: '科研' }, - - { type: 'text', msg: '人员' }, - - { type: 'text', msg: '使用' }, - - { type: 'text', msg: '。' }, - - { type: 'text', msg: '不过' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '由于' }, - - { type: 'text', msg: '南极' }, - - { type: 'text', msg: '的' }, - - { type: 'text', msg: '极端' }, - - { type: 'text', msg: '环境和' }, - - { type: 'text', msg: '极' }, - - { type: 'text', msg: '低' }, - - { type: 'text', msg: '的人口' }, - - { type: 'text', msg: '密度' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '这台' }, - - { type: 'text', msg: 'ATM' }, - - { type: 'text', msg: '机' }, - - { type: 'text', msg: '并未' }, - - { type: 'text', msg: '长期' }, - - { type: 'text', msg: '运行' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '最终' }, - - { type: 'text', msg: '被' }, - - { type: 'text', msg: '移除' }, - - { type: 'text', msg: '。\n\n' }, - - { type: 'text', msg: '**' }, - - { type: 'text', msg: '背景' }, - - { type: 'text', msg: '补充' }, - - { type: 'text', msg: ':' }, - - { type: 'text', msg: '**\n' }, - - { type: 'text', msg: '-' }, - - { type: 'text', msg: ' **' }, - - { type: 'text', msg: '麦克' }, - - { type: 'text', msg: '默' }, - - { type: 'text', msg: '多' }, - - { type: 'text', msg: '站' }, - - { type: 'text', msg: '**' }, - - { type: 'text', msg: '是美国' }, - - { type: 'text', msg: '在南' }, - - { type: 'text', msg: '极' }, - - { type: 'text', msg: '最大的' }, - - { type: 'text', msg: '科研' }, - - { type: 'text', msg: '基地' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '夏季' }, - - { type: 'text', msg: '人口' }, - - { type: 'text', msg: '可达' }, - - { type: 'text', msg: '约' }, - - { type: 'text', msg: '1' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '000' }, - - { type: 'text', msg: '人' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '冬季' }, - - { type: 'text', msg: '约' }, - - { type: 'text', msg: '200' }, - - { type: 'text', msg: '人' }, - - { type: 'text', msg: '。\n' }, - - { type: 'text', msg: '-' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '该' }, - - { type: 'text', msg: 'ATM' }, - - { type: 'text', msg: '机' }, - - { type: 'text', msg: '更多' }, - - { type: 'text', msg: '是' }, - - { type: 'text', msg: '作为一种' }, - - { type: 'text', msg: '象征' }, - - { type: 'text', msg: '性' }, - - { type: 'text', msg: '服务' }, - - { type: 'text', msg: '存在' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '实际' }, - - { type: 'text', msg: '使用' }, - - { type: 'text', msg: '频率' }, - - { type: 'text', msg: '极' }, - - { type: 'text', msg: '低' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '因为' }, - - { type: 'text', msg: '南极' }, - - { type: 'text', msg: '科' }, - - { type: 'text', msg: '考' }, - - { type: 'text', msg: '人员' }, - - { type: 'text', msg: '通常' }, - - { type: 'text', msg: '依靠' }, - - { type: 'text', msg: '预' }, - - { type: 'text', msg: '支' }, - - { type: 'text', msg: '资金' }, - - { type: 'text', msg: '或' }, - - { type: 'text', msg: '电子' }, - - { type: 'text', msg: '支付' }, - - { type: 'text', msg: '。\n' }, - - { type: 'text', msg: '-' }, - - { type: 'text', msg: ' ' }, - - { type: 'text', msg: '目前' }, - - { type: 'text', msg: '南极' }, - - { type: 'text', msg: '已' }, - - { type: 'text', msg: '无' }, - - { type: 'text', msg: '长期' }, - - { type: 'text', msg: '运行的' }, - - { type: 'text', msg: 'ATM' }, - - { type: 'text', msg: '机' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '现代' }, - - { type: 'text', msg: '科' }, - - { type: 'text', msg: '考' }, - - { type: 'text', msg: '站' }, - - { type: 'text', msg: '更多' }, - - { type: 'text', msg: '依赖' }, - - { type: 'text', msg: '非' }, - - { type: 'text', msg: '现金' }, - - { type: 'text', msg: '交易' }, - - { type: 'text', msg: '方式' }, - - { type: 'text', msg: '。\n\n' }, - - { type: 'text', msg: '南极' }, - - { type: 'text', msg: '作为' }, - - { type: 'text', msg: '非' }, - - { type: 'text', msg: '主权' }, - - { type: 'text', msg: '领土' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '其' }, - - { type: 'text', msg: '基础设施' }, - - { type: 'text', msg: '以' }, - - { type: 'text', msg: '科研' }, - - { type: 'text', msg: '和生活' }, - - { type: 'text', msg: '支持' }, - - { type: 'text', msg: '为主' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '商业' }, - - { type: 'text', msg: '金融服务' }, - - { type: 'text', msg: '非常' }, - - { type: 'text', msg: '有限' }, - - { type: 'text', msg: '。' }, - - { type: 'text', msg: '若有' }, - - { type: 'text', msg: '类似' }, - - { type: 'text', msg: '设施' }, - - { type: 'text', msg: ',' }, - - { type: 'text', msg: '通常是' }, - - { type: 'text', msg: '临时' }, - - { type: 'text', msg: '或' }, - - { type: 'text', msg: '实验' }, - - { type: 'text', msg: '性质的' }, - - { type: 'text', msg: '。' }, -]; -module.exports = chunks; diff --git a/server/chat/ssemock.js b/server/chat/ssemock.js deleted file mode 100644 index b50b984a8a..0000000000 --- a/server/chat/ssemock.js +++ /dev/null @@ -1,203 +0,0 @@ -const cors = require('cors'); -const express = require('express'); -const chunks = require('./data/normal'); -const chunksChart = require('./data/chart'); -const chunksCode = require('./data/code'); -const chunksImage = require('./data/image'); -const agentChunks = require('./data/agent'); -const chunksDoc = require('./data/docs'); - -const app = express(); -app.use(cors()); -app.use(express.json()); // 添加JSON解析中间件 - -// 统一SSE响应头设置 -const setSSEHeaders = (res) => { - res.writeHead(200, { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'X-Content-Type-Options': 'nosniff', - Connection: 'keep-alive', - 'Content-Encoding': 'none', - }); -}; - -app.post('/sse/test', (req, res) => { - res.send('Hello sse!'); -}); -app.post('/sse/agent', (req, res) => { - console.log('Received POST body:', req.body); // 打印请求体 - setSSEHeaders(res); - - // 将chunks转换为SSE格式消息 - const messages = agentChunks.map((chunk) => { - switch (chunk.type) { - case 'text': - return `event: message\ndata: ${JSON.stringify({ - type: 'text', - msg: chunk.msg, - })}\n\n`; - case 'agent': - return `event: ${chunk.type}\ndata: ${JSON.stringify({ - type: 'agent', - id: chunk.id, - state: chunk.state, - content: chunk.content, - })}\n\n`; - default: - return `event: ${chunk.type}\ndata: ${JSON.stringify({ - type: '', - id: chunk.id, - content: chunk.content, - })}\n\n`; - } - }); - - sendStream(res, messages, 300, req); -}); - -// 支持POST请求的SSE端点 -app.post('/sse/normal', (req, res) => { - console.log('Received POST body:', req.body); // 打印请求体 - setSSEHeaders(res); - - let mockdata = chunks; - const { think = false, search = false, chart = false, code = false, image = false, docs = false } = req.body; - if (chart) { - mockdata = chunksChart; - } - - if (docs) { - mockdata = chunksDoc; - } - - if (code) { - mockdata = chunksCode; - } - - if (image) { - mockdata = chunksImage; - } - - // 根据参数过滤不需要的chunk类型 - const filteredChunks = mockdata.filter((chunk) => { - if (!think && chunk.type === 'think') return false; - if (!search && chunk.type === 'search') return false; - return true; - }); - - // 将chunks转换为SSE格式消息 - const messages = filteredChunks.map((chunk) => { - switch (chunk.type) { - case 'text': - return `event: message\ndata: ${JSON.stringify({ - ...chunk, - })}\n\n`; - - case 'search': - return `event: message\ndata: ${JSON.stringify({ - type: 'search', - title: chunk.title, - content: chunk.content, - })}\n\n`; - - case 'think': - return `event: message\ndata: ${JSON.stringify(chunk)}\n\n`; - - case 'image': - return `event: media\ndata: ${JSON.stringify({ - type: 'image', - content: chunk.content, - })}\n\n`; - - case 'weather': - return `event: custom\ndata: ${JSON.stringify({ - type: 'weather', - id: chunk.id, - content: chunk.content, - })}\n\n`; - case 'chart': - return `event: custom\ndata: ${JSON.stringify({ - type: 'chart', - content: { - ...chunk.data, - id: Math.random() * 10000, - }, - })}\n\n`; - - case 'preview': - return `event: custom\ndata: ${JSON.stringify({ - type: 'preview', - content: chunk.data, - })}\n\n`; - - case 'suggestion': - return `event: suggestion\ndata: ${JSON.stringify({ - type: 'suggestion', - content: chunk.content, - })}\n\n`; - case 'error': - return `event: error\ndata: ${JSON.stringify({ - type: 'error', - content: chunk.content, - })}\n\n`; - default: - return `event: ${chunk.type}\ndata: ${chunk.content}\n\n`; - } - }); - sendStream(res, messages, 100, req); -}); - -// 带鉴权的POST请求 -app.post('/sse/auth', (req, res) => { - // 检查授权头 - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - res.status(401).json({ error: '未授权' }); - return; - } - - setSSEHeaders(res); -}); - -// 流发送工具函数 -function sendStream(res, messages, interval, req) { - let index = 0; - const timer = setInterval(() => { - if (index < messages.length) { - // 添加写入状态检查 - if (!req.socket.writable) { - console.log('Socket not writable'); - clearInterval(timer); - return res.end(); - } - res.write(messages[index]); - index++; - } else { - clearInterval(timer); - res.end(); - } - }, interval); -} - -// 模拟文件上传接口 -app.post('/file/upload', (req, res) => { - // 模拟延迟 - setTimeout(() => { - res.json({ - code: 200, - result: { - cdnurl: `https://tdesign.gtimg.com/site/avatar.jpg`, - size: 1024, - width: 800, - height: 600, - }, - }); - }, 300); -}); - -const PORT = 3000; -app.listen(PORT, () => { - console.log(`SSE Mock Server: http://localhost:${PORT}`); -}); From b14b8ee1687855dc40f182f7f60f97065c1db2ec Mon Sep 17 00:00:00 2001 From: carolin913 Date: Wed, 21 May 2025 12:02:12 +0800 Subject: [PATCH 075/228] feat(chatbot): agent demo --- .../pro-components/chat/chatbot/_example/agent.tsx | 13 +++---------- .../pro-components/chat/chatbot/_example/custom.tsx | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index 061bc5181d..5d2091265f 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -6,7 +6,6 @@ import type { ChatServiceConfig, BaseContent, ChatMessagesData, - TdChatCustomRenderConfig, } from '@tdesign-react/aigc'; import { Timeline } from 'tdesign-react'; @@ -16,13 +15,6 @@ import { ChatBot } from '@tdesign-react/aigc'; import './index.css'; -// 自定义渲染-注册插槽规则 -const customRenderConfig: TdChatCustomRenderConfig = { - agent: (content) => ({ - slotName: `${content.state}-${content.id}`, - }), -}; - const AgentTimeline = ({ steps }) => (
@@ -54,6 +46,7 @@ declare module 'tdesign-react' { { id: string; state: 'pending' | 'command' | 'result' | 'finish'; + slotName: string; content: { steps?: { step: string; @@ -98,7 +91,6 @@ export default function ChatBotReact() { }, assistant: { placement: 'left', - customRenderConfig, }, }; @@ -129,7 +121,8 @@ export default function ChatBotReact() { case 'agent': return { type: 'agent', - ...chunk.data, + slotName: `${rest.state}-${rest.id}`, + ...rest, }; default: return { diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index c355004808..edb312be73 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -15,7 +15,7 @@ import { ChatBot } from '@tdesign-react/aigc'; import TvisionTcharts from 'tvision-charts-react'; // 1、扩展自定义消息体类型 -declare module 'tdesign-react' { +declare module '@tdesign-react/aigc' { interface AIContentTypeOverrides { chart: BaseContent< 'chart', From 04ac3c001872e268c49ef0387280b695f9bc95e6 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Wed, 21 May 2025 14:11:26 +0800 Subject: [PATCH 076/228] feat(chatbot): agent fix --- .gitignore | 2 ++ .../chat/chatbot/_example/agent.tsx | 16 +++++++++------- .../chat/chatbot/_example/custom.tsx | 9 +-------- packages/tdesign-react-aigc/package.json | 2 +- packages/tdesign-react-aigc/site/vite.config.js | 3 +++ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index b09d984b21..000fac54fb 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ package-lock.json yarn.lock pnpm-lock.yaml +## mock server +server/chat/* diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index f58f2944ee..e9b8e2fd06 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -15,6 +15,11 @@ import { ChatBot } from '@tdesign-react/aigc'; import './index.css'; +const endpoint = (() => { + const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname); + return isLocal ? 'http://localhost:3000' : 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com'; +})(); + const AgentTimeline = ({ steps }) => (
@@ -46,7 +51,6 @@ declare module 'tdesign-react' { { id: string; state: 'pending' | 'command' | 'result' | 'finish'; - slotName: string; content: { steps?: { step: string; @@ -97,7 +101,7 @@ export default function ChatBotReact() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/agent', + endpoint: `${endpoint}/sse/agent`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { @@ -121,7 +125,6 @@ export default function ChatBotReact() { case 'agent': return { type: 'agent', - slotName: `${rest.state}-${rest.id}`, ...rest, }; default: @@ -136,7 +139,6 @@ export default function ChatBotReact() { const { prompt } = innerParams; return { headers: { - 'Content-Type': 'text/event-stream', 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify({ @@ -219,11 +221,11 @@ export default function ChatBotReact() { }} > {mockMessage - ?.map((data) => - data.content.map((item) => { + ?.map((msg) => + msg.content.map((item) => { if (item.type === 'agent') { return ( -
+
); diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index 6f4d14bc1d..72de89a422 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -28,13 +28,6 @@ declare module '@tdesign-react/aigc' { } } -// 自定义渲染-注册插槽规则(可选) -// const customRenderConfig: TdChatCustomRenderConfig = { -// chart: (content) => ({ -// slotName: `${content.type}-${content.data.id}`, -// }), -// }; - // 2、自定义渲染图表的组件 const ChartDemo = ({ data }) => (
diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 8a7d28a4c4..3f3c46ca5b 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -50,7 +50,7 @@ "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", - "@tencent/tdesign-chatbot": "1.0.0-beta.57", + "@tencent/tdesign-chatbot": "1.0.0-beta.58", "@types/sortablejs": "^1.10.7", "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.1.3", diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js index 713deafe9c..e78c312e41 100644 --- a/packages/tdesign-react-aigc/site/vite.config.js +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -26,6 +26,9 @@ export default ({ mode }) => resolve: { alias: { '@tdesign-react/aigc': path.resolve(__dirname, '../../pro-components/chat'), + '@tdesign/react-aigc-site': path.resolve(__dirname, './'), + 'tdesign-react': path.resolve(__dirname, '../../components'), + 'tdesign-react/es': path.resolve(__dirname, '../../tdesign-react'), }, }, build: { From a51c58fc4e74b3946b2f6e1f420e36ae2705a3d0 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Wed, 21 May 2025 15:34:08 +0800 Subject: [PATCH 077/228] feat(chatbot): endpoint --- packages/pro-components/chat/chatbot/_example/agent.tsx | 6 +----- packages/pro-components/chat/chatbot/_example/basic.tsx | 3 ++- packages/pro-components/chat/chatbot/_example/code.tsx | 3 ++- packages/pro-components/chat/chatbot/_example/custom.tsx | 4 ++-- packages/pro-components/chat/chatbot/_example/docs.tsx | 4 +++- .../pro-components/chat/chatbot/_example/hookComponent.tsx | 3 ++- packages/pro-components/chat/chatbot/_example/image.tsx | 3 ++- packages/pro-components/chat/chatbot/_example/research.tsx | 1 + packages/pro-components/chat/chatbot/_example/utils.ts | 4 ++++ 9 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 packages/pro-components/chat/chatbot/_example/utils.ts diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index e9b8e2fd06..4283dd465c 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -14,11 +14,7 @@ import { CheckCircleFilledIcon } from 'tdesign-icons-react'; import { ChatBot } from '@tdesign-react/aigc'; import './index.css'; - -const endpoint = (() => { - const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname); - return isLocal ? 'http://localhost:3000' : 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com'; -})(); +import { endpoint } from './utils'; const AgentTimeline = ({ steps }) => (
diff --git a/packages/pro-components/chat/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx index 6723fab442..a7361e1953 100644 --- a/packages/pro-components/chat/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -11,6 +11,7 @@ import { type TdChatbotApi, } from '@tdesign-react/aigc'; import { Button, Space } from 'tdesign-react'; +import { endpoint } from './utils'; // 默认初始化消息 const mockData: ChatMessagesData[] = [ @@ -104,7 +105,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: `${endpoint}/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/code.tsx b/packages/pro-components/chat/chatbot/_example/code.tsx index a95be0e16f..c871575897 100644 --- a/packages/pro-components/chat/chatbot/_example/code.tsx +++ b/packages/pro-components/chat/chatbot/_example/code.tsx @@ -10,6 +10,7 @@ import { ChatServiceConfig, } from '@tdesign-react/aigc'; import Login from './components/login'; +import { endpoint } from './utils'; // 默认初始化消息 const mockData: ChatMessagesData[] = [ @@ -134,7 +135,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: `${endpoint}/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index 72de89a422..a876de3419 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -11,8 +11,8 @@ import type { } from '@tdesign-react/aigc'; import { Button, Space } from 'tdesign-react'; import { ChatBot } from '@tdesign-react/aigc'; - import TvisionTcharts from 'tvision-charts-react'; +import { endpoint } from './utils'; // 1、扩展自定义消息体类型 declare module '@tdesign-react/aigc' { @@ -79,7 +79,7 @@ export default function ChatBotReact() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: `${endpoint}/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/docs.tsx b/packages/pro-components/chat/chatbot/_example/docs.tsx index 87c66c74f9..2b60130e90 100644 --- a/packages/pro-components/chat/chatbot/_example/docs.tsx +++ b/packages/pro-components/chat/chatbot/_example/docs.tsx @@ -11,6 +11,8 @@ import type { TdChatbotApi, } from '@tdesign-react/aigc'; import { ChatBot } from '@tdesign-react/aigc'; +import { endpoint } from './utils'; + // 默认初始化消息 const mockData: ChatMessagesData[] = [ { @@ -57,7 +59,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: `${endpoint}/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index f06bcaac92..7e491acd74 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -15,6 +15,7 @@ import { useChat, } from '@tdesign-react/aigc'; import { getMessageContentForCopy, TdChatActionsName } from '@tencent/tdesign-chatbot'; +import { endpoint } from './utils'; export default function ComponentsBuild() { const listRef = useRef(null); @@ -25,7 +26,7 @@ export default function ComponentsBuild() { // 聊天服务配置 chatServiceConfig: { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: `${endpoint}/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/image.tsx b/packages/pro-components/chat/chatbot/_example/image.tsx index fc6819dbbd..8e2d764813 100644 --- a/packages/pro-components/chat/chatbot/_example/image.tsx +++ b/packages/pro-components/chat/chatbot/_example/image.tsx @@ -15,6 +15,7 @@ import type { } from '@tdesign-react/aigc'; import { ImageViewer, Skeleton, ImageViewerProps, Button, Dropdown, Space, Image } from 'tdesign-react'; import { ChatBot } from '@tdesign-react/aigc'; +import { endpoint } from './utils'; const RatioOptions = [ { @@ -161,7 +162,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal', + endpoint: `${endpoint}/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/research.tsx b/packages/pro-components/chat/chatbot/_example/research.tsx index 87c66c74f9..e2e59263f5 100644 --- a/packages/pro-components/chat/chatbot/_example/research.tsx +++ b/packages/pro-components/chat/chatbot/_example/research.tsx @@ -11,6 +11,7 @@ import type { TdChatbotApi, } from '@tdesign-react/aigc'; import { ChatBot } from '@tdesign-react/aigc'; + // 默认初始化消息 const mockData: ChatMessagesData[] = [ { diff --git a/packages/pro-components/chat/chatbot/_example/utils.ts b/packages/pro-components/chat/chatbot/_example/utils.ts new file mode 100644 index 0000000000..bc6e744a5e --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/utils.ts @@ -0,0 +1,4 @@ +export const endpoint = (() => { + const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname); + return isLocal ? 'http://localhost:3000' : 'https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com'; +})(); From bae9a9dacb2d2e1e3d42bfa5187e486e341d1f1b Mon Sep 17 00:00:00 2001 From: carolin913 Date: Wed, 21 May 2025 20:29:20 +0800 Subject: [PATCH 078/228] feat(chatactionbar): style usedynamic --- package.json | 3 +- .../chat/_util/useDynamicStyle.ts | 41 +++++++++++++++++++ .../chat/chat-actionbar/_example/style.tsx | 36 ++++++++++++++++ .../chat/chat-actionbar/chat-actionbar.md | 9 +++- .../chat/chat-sender/_example/custom.tsx | 33 ++++----------- .../chat/chat-thinking/chat-thinking.md | 2 +- packages/tdesign-react-aigc/package.json | 2 +- packages/tdesign-react/package.json | 6 --- 8 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 packages/pro-components/chat/_util/useDynamicStyle.ts create mode 100644 packages/pro-components/chat/chat-actionbar/_example/style.tsx diff --git a/package.json b/package.json index 409f291610..5097aeb11e 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,6 @@ "tdesign-icons-react": "0.5.0", "tinycolor2": "^1.4.2", "tslib": "~2.3.1", - "validator": "~13.7.0", - "@tencent/tdesign-chatbot": "1.0.0-beta.56" + "validator": "~13.7.0" } } diff --git a/packages/pro-components/chat/_util/useDynamicStyle.ts b/packages/pro-components/chat/_util/useDynamicStyle.ts new file mode 100644 index 0000000000..a2a6ee5c05 --- /dev/null +++ b/packages/pro-components/chat/_util/useDynamicStyle.ts @@ -0,0 +1,41 @@ +import { useRef, useEffect, MutableRefObject } from 'react'; + +type StyleVariables = Record; + +// 用于动态管理组件作用域样式 +export const useDynamicStyle = (elementRef: MutableRefObject, cssVariables: StyleVariables) => { + const styleId = useRef(`dynamic-styles-${Math.random().toString(36).slice(2, 11)}`); + + // 生成带作用域的CSS样式 + const generateScopedStyles = (vars: StyleVariables) => { + const variables = Object.entries(vars) + .map(([key, value]) => `${key}: ${value};`) + .join('\n'); + + return ` + .${styleId.current} { + ${variables} + } + `; + }; + + useEffect(() => { + if (!elementRef?.current) return; + const styleElement = document.createElement('style'); + styleElement.innerHTML = generateScopedStyles(cssVariables); + document.head.appendChild(styleElement); + + // 绑定样式类到目标元素 + const currentElement = elementRef.current; + if (currentElement) { + currentElement.classList.add(styleId.current); + } + + return () => { + document.head.removeChild(styleElement); + if (currentElement) { + currentElement.classList.remove(styleId.current); + } + }; + }, [cssVariables]); +}; diff --git a/packages/pro-components/chat/chat-actionbar/_example/style.tsx b/packages/pro-components/chat/chat-actionbar/_example/style.tsx new file mode 100644 index 0000000000..93cf72a1e0 --- /dev/null +++ b/packages/pro-components/chat/chat-actionbar/_example/style.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Space } from 'tdesign-react'; +import { ChatActionBar } from '@tdesign-react/aigc'; +import { useDynamicStyle } from '../../_util/useDynamicStyle'; + +const ChatActionBarExample = () => { + const barRef = React.useRef(null); + + // 这里是为了演示样式修改不影响其他Demo,实际项目中直接设置css变量到:root即可 + useDynamicStyle(barRef, { + '--td-chat-item-actions-list-border': 'none', + '--td-chat-item-actions-list-bg': 'none', + '--td-chat-item-actions-item-hover-bg': '#f3f3f3', + }); + + const onActions = (name, data) => { + console.log('消息事件触发:', name, data); + }; + + return ( + + + + ); +}; + +export default ChatActionBarExample; diff --git a/packages/pro-components/chat/chat-actionbar/chat-actionbar.md b/packages/pro-components/chat/chat-actionbar/chat-actionbar.md index 97bc886139..9fff41a19a 100644 --- a/packages/pro-components/chat/chat-actionbar/chat-actionbar.md +++ b/packages/pro-components/chat/chat-actionbar/chat-actionbar.md @@ -10,13 +10,20 @@ spline: aigc {{ base }} +## 样式调整 +支持通过css变量修改样式, +支持通过`tooltipProps`属性设置提示浮层的样式 + +{{ style }} + ## 自定义 -支持调整顺序,展示指定项 +目前仅支持有限的自定义,包括调整顺序,展示指定项 {{ custom }} + ## API ### ChatActionBar Props diff --git a/packages/pro-components/chat/chat-sender/_example/custom.tsx b/packages/pro-components/chat/chat-sender/_example/custom.tsx index 8305adcb39..480de64be4 100644 --- a/packages/pro-components/chat/chat-sender/_example/custom.tsx +++ b/packages/pro-components/chat/chat-sender/_example/custom.tsx @@ -3,6 +3,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { EnterIcon, InternetIcon, AttachIcon, CloseIcon, ArrowUpIcon, StopIcon } from 'tdesign-icons-react'; import { ChatSender } from '@tdesign-react/aigc'; import { Space, Button, Tag, Dropdown, Tooltip, UploadFile } from 'tdesign-react'; +import { useDynamicStyle } from '../../_util/useDynamicStyle'; const options = [ { @@ -31,33 +32,13 @@ const ChatSenderExample = () => { const [showRef, setShowRef] = useState(true); const [activeR1, setR1Active] = useState(false); const [activeSearch, setSearchActive] = useState(false); - const styleId = useRef(`chat-sender-styles-${Math.random().toString(36).substr(2, 9)}`); - // 使用变量生成自定义组件样式 - const generateScopedStyles = () => ` - .${styleId.current} { - --td-text-color-placeholder: #DFE2E7; - --td-bg-color-secondarycontainer: #fff; - --td-chat-input-background: #fff; - } - `; - - useEffect(() => { - const styleElement = document.createElement('style'); - styleElement.innerHTML = generateScopedStyles(); - document.head.appendChild(styleElement); - - // 为容器添加唯一类名 - if (senderRef.current) { - senderRef.current.classList.add(styleId.current); - } - return () => { - document.head.removeChild(styleElement); - if (senderRef.current) { - senderRef.current.classList.remove(styleId.current); - } - }; - }, []); + // 这里是为了演示样式修改不影响其他Demo,实际项目中直接设置css变量到:root即可 + useDynamicStyle(senderRef, { + '--td-text-color-placeholder': '#DFE2E7', + '--td-bg-color-secondarycontainer': '#fff', + '--td-chat-input-background': ' #fff', + }); // 输入变化处理 const handleChange = (e) => { diff --git a/packages/pro-components/chat/chat-thinking/chat-thinking.md b/packages/pro-components/chat/chat-thinking/chat-thinking.md index 70eb37a5d6..ec8f705311 100644 --- a/packages/pro-components/chat/chat-thinking/chat-thinking.md +++ b/packages/pro-components/chat/chat-thinking/chat-thinking.md @@ -17,7 +17,7 @@ spline: navigation ## 样式设置 支持通过`layout`来设置思考过程的布局方式 -支持通过`animation`来设置思考过程的动画效果 +支持通过`animation`来设置思考内容加载过程的动画效果 {{ style }} diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 3f3c46ca5b..e431778d0e 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -50,7 +50,7 @@ "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", - "@tencent/tdesign-chatbot": "1.0.0-beta.58", + "@tencent/tdesign-chatbot": "1.0.0-beta.61", "@types/sortablejs": "^1.10.7", "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.1.3", diff --git a/packages/tdesign-react/package.json b/packages/tdesign-react/package.json index a1e4f2494f..1a91ed63c3 100644 --- a/packages/tdesign-react/package.json +++ b/packages/tdesign-react/package.json @@ -76,7 +76,6 @@ "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", - "@tencent/tdesign-chatbot": "1.0.0-beta.47", "@types/sortablejs": "^1.10.7", "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.1.3", @@ -94,10 +93,5 @@ "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0" - }, - "devDependencies": { - "cors": "^2.8.5", - "tvision-charts-react": "^3.3.12", - "express": "^4.17.3" } } From 2f3e80267bcbf23806581be9caa37e8f42dfa8eb Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 26 May 2025 11:44:47 +0800 Subject: [PATCH 079/228] feat(chat): change deps to tdesign-web-components and add import --- .../chat/chat-actionbar/index.ts | 5 +++-- .../chat/chat-attachments/index.ts | 5 +++-- .../chat/chat-filecard/index.ts | 5 +++-- .../pro-components/chat/chat-loading/index.ts | 5 +++-- .../chat/chat-markdown/index.ts | 5 +++-- .../chat/chat-message/_example/action.tsx | 2 +- .../pro-components/chat/chat-message/index.ts | 4 ++-- .../chat/chat-sender/_example/custom.tsx | 2 +- .../pro-components/chat/chat-sender/index.ts | 9 ++++----- .../chat/chat-thinking/_example/base.tsx | 2 +- .../chat/chat-thinking/_example/style.tsx | 2 +- .../chat/chat-thinking/index.ts | 5 +++-- .../chat/chatbot/_example/agent.tsx | 4 ++-- .../chat/chatbot/_example/hookComponent.tsx | 2 +- .../pro-components/chat/chatbot/chatbot.md | 13 +++++++++++++ packages/pro-components/chat/chatbot/index.ts | 19 ++++++++----------- .../pro-components/chat/chatbot/useChat.ts | 6 +++--- packages/pro-components/chat/index.ts | 1 + packages/tdesign-react-aigc/package.json | 2 +- 19 files changed, 57 insertions(+), 41 deletions(-) diff --git a/packages/pro-components/chat/chat-actionbar/index.ts b/packages/pro-components/chat/chat-actionbar/index.ts index 6cf65759c4..2ecadc6a60 100644 --- a/packages/pro-components/chat/chat-actionbar/index.ts +++ b/packages/pro-components/chat/chat-actionbar/index.ts @@ -1,4 +1,5 @@ -import { TdChatActionProps } from '@tencent/tdesign-chatbot'; +import { TdChatActionProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-action'; import reactify from '../_util/reactify'; export const ChatActionBar: React.ForwardRefExoticComponent< @@ -9,4 +10,4 @@ export const ChatActionBar: React.ForwardRefExoticComponent< > = reactify('t-chat-action'); export default ChatActionBar; -export type { TdChatActionProps } from '@tencent/tdesign-chatbot'; +export type { TdChatActionProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-attachments/index.ts b/packages/pro-components/chat/chat-attachments/index.ts index 7e20dd8860..8f8fe02038 100644 --- a/packages/pro-components/chat/chat-attachments/index.ts +++ b/packages/pro-components/chat/chat-attachments/index.ts @@ -1,4 +1,5 @@ -import { TdAttachmentsProps } from '@tencent/tdesign-chatbot'; +import { TdAttachmentsProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/attachments'; import reactify from '../_util/reactify'; export const ChatAttachments: React.ForwardRefExoticComponent< @@ -7,4 +8,4 @@ export const ChatAttachments: React.ForwardRefExoticComponent< export default ChatAttachments; -export type { TdAttachmentsProps, TdAttachmentItem } from '@tencent/tdesign-chatbot'; +export type { TdAttachmentsProps, TdAttachmentItem } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-filecard/index.ts b/packages/pro-components/chat/chat-filecard/index.ts index 5b386b3fdf..7dcf2d03cd 100644 --- a/packages/pro-components/chat/chat-filecard/index.ts +++ b/packages/pro-components/chat/chat-filecard/index.ts @@ -1,4 +1,5 @@ -import { TdFileCardProps } from '@tencent/tdesign-chatbot'; +import { TdFileCardProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/filecard'; import reactify from '../_util/reactify'; export const Filecard: React.ForwardRefExoticComponent< @@ -6,4 +7,4 @@ export const Filecard: React.ForwardRefExoticComponent< > = reactify('t-filecard'); export default Filecard; -export type { TdFileCardProps } from '@tencent/tdesign-chatbot'; +export type { TdFileCardProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-loading/index.ts b/packages/pro-components/chat/chat-loading/index.ts index 8ba95e2b3e..dca6150429 100644 --- a/packages/pro-components/chat/chat-loading/index.ts +++ b/packages/pro-components/chat/chat-loading/index.ts @@ -1,4 +1,5 @@ -import { TdChatLoadingProps } from '@tencent/tdesign-chatbot'; +import { TdChatLoadingProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-loading'; import reactify from '../_util/reactify'; export const ChatLoading: React.ForwardRefExoticComponent< @@ -6,4 +7,4 @@ export const ChatLoading: React.ForwardRefExoticComponent< > = reactify('t-chat-loading'); export default ChatLoading; -export type { TdChatLoadingProps } from '@tencent/tdesign-chatbot'; +export type { TdChatLoadingProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-markdown/index.ts b/packages/pro-components/chat/chat-markdown/index.ts index 61964da56f..bd523d1b0c 100644 --- a/packages/pro-components/chat/chat-markdown/index.ts +++ b/packages/pro-components/chat/chat-markdown/index.ts @@ -1,4 +1,5 @@ -import { TdChatMarkdownContentProps } from '@tencent/tdesign-chatbot'; +import { TdChatMarkdownContentProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-message/content/markdown-content'; import reactify from '../_util/reactify'; export const ChatMarkdown: React.ForwardRefExoticComponent< @@ -6,4 +7,4 @@ export const ChatMarkdown: React.ForwardRefExoticComponent< > = reactify('t-chat-md-content'); export default ChatMarkdown; -export type { TdChatMarkdownContentProps } from '@tencent/tdesign-chatbot'; +export type { TdChatMarkdownContentProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-message/_example/action.tsx b/packages/pro-components/chat/chat-message/_example/action.tsx index d0f1f114a7..f7b44e2c0b 100644 --- a/packages/pro-components/chat/chat-message/_example/action.tsx +++ b/packages/pro-components/chat/chat-message/_example/action.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Space } from 'tdesign-react'; import { ChatActionBar, ChatMessage } from '@tdesign-react/aigc'; -import { AIMessage, getMessageContentForCopy } from '@tencent/tdesign-chatbot'; +import { AIMessage, getMessageContentForCopy } from 'tdesign-web-components'; const message: AIMessage = { id: '123123', diff --git a/packages/pro-components/chat/chat-message/index.ts b/packages/pro-components/chat/chat-message/index.ts index 3947957fc9..605cf6a69d 100644 --- a/packages/pro-components/chat/chat-message/index.ts +++ b/packages/pro-components/chat/chat-message/index.ts @@ -1,5 +1,5 @@ -import { type TdChatItemProps } from '@tencent/tdesign-chatbot'; -import '@tencent/tdesign-chatbot/lib/style/index.css'; +import { type TdChatItemProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-message'; import reactify from '../_util/reactify'; export const ChatMessage: React.ForwardRefExoticComponent< diff --git a/packages/pro-components/chat/chat-sender/_example/custom.tsx b/packages/pro-components/chat/chat-sender/_example/custom.tsx index 480de64be4..b8f198169d 100644 --- a/packages/pro-components/chat/chat-sender/_example/custom.tsx +++ b/packages/pro-components/chat/chat-sender/_example/custom.tsx @@ -1,4 +1,4 @@ -import { TdAttachmentItem } from '@tencent/tdesign-chatbot'; +import { TdAttachmentItem } from 'tdesign-web-components'; import React, { useRef, useState, useEffect } from 'react'; import { EnterIcon, InternetIcon, AttachIcon, CloseIcon, ArrowUpIcon, StopIcon } from 'tdesign-icons-react'; import { ChatSender } from '@tdesign-react/aigc'; diff --git a/packages/pro-components/chat/chat-sender/index.ts b/packages/pro-components/chat/chat-sender/index.ts index 0e3aa8baa8..9c8a017f7a 100644 --- a/packages/pro-components/chat/chat-sender/index.ts +++ b/packages/pro-components/chat/chat-sender/index.ts @@ -1,11 +1,10 @@ -import { TdChatSenderApi, TdChatSenderProps } from '@tencent/tdesign-chatbot'; -import '@tencent/tdesign-chatbot/lib/style/index.css'; +import { TdChatSenderProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-sender'; import reactify from '../_util/reactify'; export const ChatSender: React.ForwardRefExoticComponent< - Omit & - React.RefAttributes + Omit & React.RefAttributes > = reactify('t-chat-sender'); export default ChatSender; -export type { TdChatSenderProps, TdChatSenderApi } from '@tencent/tdesign-chatbot'; +export type { TdChatSenderProps, TdChatSenderApi } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-thinking/_example/base.tsx b/packages/pro-components/chat/chat-thinking/_example/base.tsx index 4c9f77d4cc..da5ba87b43 100644 --- a/packages/pro-components/chat/chat-thinking/_example/base.tsx +++ b/packages/pro-components/chat/chat-thinking/_example/base.tsx @@ -1,4 +1,4 @@ -import { type MessageStatus } from '@tencent/tdesign-chatbot'; +import { type MessageStatus } from 'tdesign-web-components'; import React, { useState, useEffect, useRef } from 'react'; import { ChatThinking } from '@tdesign-react/aigc'; diff --git a/packages/pro-components/chat/chat-thinking/_example/style.tsx b/packages/pro-components/chat/chat-thinking/_example/style.tsx index 102bd5cfa8..7b469ca07b 100644 --- a/packages/pro-components/chat/chat-thinking/_example/style.tsx +++ b/packages/pro-components/chat/chat-thinking/_example/style.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Radio, Space } from 'tdesign-react'; import { ChatThinking } from '@tdesign-react/aigc'; -import type { TdChatThinkContentProps, MessageStatus } from '@tencent/tdesign-chatbot'; +import type { TdChatThinkContentProps, MessageStatus } from 'tdesign-web-components'; const fullText = '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。'; diff --git a/packages/pro-components/chat/chat-thinking/index.ts b/packages/pro-components/chat/chat-thinking/index.ts index 9ebe17071d..58fac59d62 100644 --- a/packages/pro-components/chat/chat-thinking/index.ts +++ b/packages/pro-components/chat/chat-thinking/index.ts @@ -1,4 +1,5 @@ -import { TdChatThinkContentProps } from '@tencent/tdesign-chatbot'; +import { TdChatThinkContentProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-message/content/thinking-content'; import reactify from '../_util/reactify'; const ChatThinkContent: React.ForwardRefExoticComponent< @@ -9,4 +10,4 @@ export const ChatThinking = ChatThinkContent; export default ChatThinking; -export type { TdChatThinkContentProps } from '@tencent/tdesign-chatbot'; +export type { TdChatThinkContentProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index 4283dd465c..4190dad273 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -218,10 +218,10 @@ export default function ChatBotReact() { > {mockMessage ?.map((msg) => - msg.content.map((item) => { + msg.content.map((item, index) => { if (item.type === 'agent') { return ( -
+
); diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 7e491acd74..782cbd88af 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -14,7 +14,7 @@ import { isAIMessage, useChat, } from '@tdesign-react/aigc'; -import { getMessageContentForCopy, TdChatActionsName } from '@tencent/tdesign-chatbot'; +import { getMessageContentForCopy, TdChatActionsName } from 'tdesign-web-components'; import { endpoint } from './utils'; export default function ComponentsBuild() { diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index ea6c6a2160..b85a3e7b4e 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -5,8 +5,21 @@ isComponent: true spline: navigation --- +## 基本用法 + +### 标准化集成 +组件内置状态管理,SSE解析,自动处理消息内容渲染与交互逻辑,可开箱即用快速集成实现标准聊天界面。本示例演示了如何快速创建一个具备以下功能的智能对话组件: + - 初始化预设消息 + - 预设消息内容渲染支持(markdown、搜索、思考、建议等) + - 与服务端的SSE(Server-Sent Events)通信,支持流式消息响应 + - 自定义流式内容结构解析 + - 自定义请求参数处理 + - 常用消息操作处理及回调(复制、重试、点赞/点踩) + - 支持手动触发填入prompt, 重新生成,发送消息等 + {{ basic }} + ### 组合式用法 可以通过 `useChat` Hook提供的对话引擎实例及状态控制方法,同时自行组合拼装`ChatList`,`ChatMessage`, `ChatSender`等组件集成聊天界面,适合需要深度定制组件结构和消息处理流程的场景 {{ hookComponent }} diff --git a/packages/pro-components/chat/chatbot/index.ts b/packages/pro-components/chat/chatbot/index.ts index fd623b66d2..8934188638 100644 --- a/packages/pro-components/chat/chatbot/index.ts +++ b/packages/pro-components/chat/chatbot/index.ts @@ -1,19 +1,16 @@ -import '@tencent/tdesign-chatbot/lib/chatbot'; -import '@tencent/tdesign-chatbot/lib/chat-message/content/search-content'; -import '@tencent/tdesign-chatbot/lib/chat-message/content/suggestion-content'; -import '@tencent/tdesign-chatbot/lib/chat-message/content/markdown-content'; -import '@tencent/tdesign-chatbot/lib/style/index.css'; +import 'tdesign-web-components/lib/chatbot'; +import 'tdesign-web-components/lib/chat-message/content/search-content'; +import 'tdesign-web-components/lib/chat-message/content/suggestion-content'; import type { TdChatbotApi, - TdChatListApi, TdChatListProps, TdChatProps, TdChatSearchContentProps, TdChatSuggestionContentProps, -} from '@tencent/tdesign-chatbot'; +} from 'tdesign-web-components'; import reactify from '../_util/reactify'; -export * from '@tencent/tdesign-chatbot/lib/chatbot/core/utils'; +export * from 'tdesign-web-components/lib/chatbot/core/utils'; export * from './useChat'; @@ -30,8 +27,8 @@ const ChatSuggestionContent: React.ForwardRefExoticComponent< > = reactify('t-chat-suggestion-content'); const ChatList: React.ForwardRefExoticComponent< - Omit & React.RefAttributes -> = reactify('t-chat-list'); + Omit & React.RefAttributes +> = reactify('t-chat-list'); export { ChatBot, ChatSearchContent, ChatSuggestionContent, ChatList }; -// export type * from '@tencent/tdesign-chatbot/lib/chatbot/type'; +// export type * from 'tdesign-web-components/lib/chatbot/type'; diff --git a/packages/pro-components/chat/chatbot/useChat.ts b/packages/pro-components/chat/chatbot/useChat.ts index aa48e391e5..c9d7746307 100644 --- a/packages/pro-components/chat/chatbot/useChat.ts +++ b/packages/pro-components/chat/chatbot/useChat.ts @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; -import type { ChatMessagesData, ChatStatus } from '@tencent/tdesign-chatbot/lib/chatbot/core/type'; -import { TdChatProps } from '@tencent/tdesign-chatbot'; -import ChatEngine from '@tencent/tdesign-chatbot/lib/chatbot/core'; +import type { ChatMessagesData, ChatStatus } from 'tdesign-web-components/lib/chatbot/core/type'; +import { TdChatProps } from 'tdesign-web-components'; +import ChatEngine from 'tdesign-web-components/lib/chatbot/core'; export type IUseChat = Pick; diff --git a/packages/pro-components/chat/index.ts b/packages/pro-components/chat/index.ts index 5de1c62c51..004109d8b8 100644 --- a/packages/pro-components/chat/index.ts +++ b/packages/pro-components/chat/index.ts @@ -1,3 +1,4 @@ +import 'tdesign-web-components/lib/style/index.css'; export * from './chatbot'; export * from './chat-actionbar'; export * from './chat-attachments'; diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index e431778d0e..ada1d98ae3 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -50,7 +50,7 @@ "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", - "@tencent/tdesign-chatbot": "1.0.0-beta.61", + "tdesign-web-components": "1.0.0", "@types/sortablejs": "^1.10.7", "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.1.3", From 8acd6aaf0425d3100ac5ae28777d08da9da2d1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 15:52:14 +0800 Subject: [PATCH 080/228] chore: build script --- .../build/tdesign-react-aigc/rollup.config.js | 42 +++---------------- package.json | 1 + packages/pro-components/chat/index.ts | 1 - packages/pro-components/chat/style/index.js | 1 + packages/tdesign-react-aigc/package.json | 23 ++-------- packages/tdesign-react-aigc/site/src/App.jsx | 9 +--- packages/tdesign-react-aigc/site/src/main.jsx | 2 +- tsconfig.json | 3 -- 8 files changed, 14 insertions(+), 68 deletions(-) create mode 100644 packages/pro-components/chat/style/index.js diff --git a/internal/build/tdesign-react-aigc/rollup.config.js b/internal/build/tdesign-react-aigc/rollup.config.js index 37529ebba2..62f36d81dc 100644 --- a/internal/build/tdesign-react-aigc/rollup.config.js +++ b/internal/build/tdesign-react-aigc/rollup.config.js @@ -15,7 +15,6 @@ import { resolve } from 'path'; import pkg from '../../../packages/tdesign-react-aigc/package.json'; -console.log(pkg.dependencies, 'pkg.dependencies'); const name = 'tdesign'; const externalDeps = Object.keys(pkg.dependencies || {}); const externalPeerDeps = Object.keys(pkg.peerDependencies || {}); @@ -29,7 +28,6 @@ const inputList = [ 'packages/pro-components/chat/**/*.ts', 'packages/pro-components/chat/**/*.tsx', '!packages/pro-components/chat/**/_example', - '!packages/pro-components/chat/**/_example-js', '!packages/pro-components/chat/**/*.d.ts', '!packages/pro-components/chat/**/__tests__', '!packages/pro-components/chat/**/_usage', @@ -64,36 +62,6 @@ const getPlugins = ({ env, isProd = false, ignoreLess = true, extractMultiCss = }), ]; - if (extractMultiCss) { - plugins.push( - staticImport({ - baseDir: 'packages/pro-components/chat', - include: ['packages/components/**/style/css.js'], - }), - ignoreImport({ - include: ['packages/pro-components/chat/*/style/*'], - body: 'import "./css.js";', - }), - ); - } else if (ignoreLess) { - plugins.push(ignoreImport({ extensions: ['*.less'] })); - } else { - plugins.push( - staticImport({ - baseDir: 'packages/pro-components/chat', - include: ['packages/pro-components/chat/**/style/index.js'], - }), - staticImport({ - baseDir: 'packages/common', - include: ['packages/common/style/web/**/*.less'], - }), - ignoreImport({ - include: ['packages/pro-components/chat/*/style/*'], - body: 'import "./style/index.js";', - }), - ); - } - if (env) { plugins.push( replace({ @@ -121,11 +89,11 @@ const getPlugins = ({ env, isProd = false, ignoreLess = true, extractMultiCss = }; const cssConfig = { - input: ['packages/components/**/style/index.js'], - plugins: [multiInput({ relative: 'packages/components/' }), styles({ mode: 'extract' })], + input: ['packages/pro-components/chat/style/index.js'], + plugins: [multiInput({ relative: 'packages/pro-components/chat' }), styles({ mode: 'extract' })], output: { banner, - dir: './packages/tdesign-react/es', + dir: 'packages/tdesign-react-aigc/es/', sourcemap: true, assetFileNames: '[name].css', }, @@ -140,11 +108,11 @@ const esConfig = { plugins: [multiInput({ relative: 'packages/pro-components/chat' })].concat(getPlugins({ extractMultiCss: true })), output: { banner, - dir: 'packages/@tdesign-react/aigc/es/', + dir: 'packages/tdesign-react-aigc/es/', format: 'esm', sourcemap: true, chunkFileNames: '_chunks/dep-[hash].js', }, }; -export default [esConfig]; +export default [esConfig, cssConfig]; diff --git a/package.json b/package.json index 5097aeb11e..62c735d0c8 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "react-fast-compare": "^3.2.2", "sortablejs": "^1.15.0", "tdesign-icons-react": "0.5.0", + "tdesign-react": "workspace:^", "tinycolor2": "^1.4.2", "tslib": "~2.3.1", "validator": "~13.7.0" diff --git a/packages/pro-components/chat/index.ts b/packages/pro-components/chat/index.ts index 004109d8b8..5de1c62c51 100644 --- a/packages/pro-components/chat/index.ts +++ b/packages/pro-components/chat/index.ts @@ -1,4 +1,3 @@ -import 'tdesign-web-components/lib/style/index.css'; export * from './chatbot'; export * from './chat-actionbar'; export * from './chat-attachments'; diff --git a/packages/pro-components/chat/style/index.js b/packages/pro-components/chat/style/index.js new file mode 100644 index 0000000000..3e9cfdf349 --- /dev/null +++ b/packages/pro-components/chat/style/index.js @@ -0,0 +1 @@ +import 'tdesign-web-components/lib/style/index.css'; diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index ada1d98ae3..d0d0b06e23 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,6 +1,6 @@ { "name": "@tdesign-react/aigc", - "version": "0.0.1", + "version": "0.1.0", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", @@ -49,29 +49,14 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "@popperjs/core": "~2.11.2", "tdesign-web-components": "1.0.0", - "@types/sortablejs": "^1.10.7", - "@types/tinycolor2": "^1.4.3", - "@types/validator": "^13.1.3", "classnames": "~2.5.1", - "dayjs": "1.11.10", - "hoist-non-react-statics": "~3.3.2", - "lodash-es": "^4.17.21", - "mitt": "^3.0.0", - "raf": "~3.4.1", - "react-is": "^18.2.0", - "react-fast-compare":"^3.2.2", - "react-transition-group": "~4.4.1", - "sortablejs": "^1.15.0", - "tdesign-icons-react": "0.5.0", - "tdesign-react":"^1.12.1", - "tinycolor2": "^1.4.2", - "tslib": "~2.3.1", - "validator": "~13.7.0" + "lodash-es": "^4.17.21" }, "devDependencies": { "cors": "^2.8.5", + "tdesign-icons-react": "0.5.0", + "tdesign-react": "^1.12.1", "tvision-charts-react": "^3.3.12", "express": "^4.17.3" } diff --git a/packages/tdesign-react-aigc/site/src/App.jsx b/packages/tdesign-react-aigc/site/src/App.jsx index 8862beb4b7..96707cc163 100644 --- a/packages/tdesign-react-aigc/site/src/App.jsx +++ b/packages/tdesign-react-aigc/site/src/App.jsx @@ -1,11 +1,9 @@ -import React, { useEffect, useRef, useState, lazy, Suspense } from 'react'; +import React, { useEffect, useRef, lazy, Suspense } from 'react'; import { BrowserRouter, Routes, Navigate, Route, useLocation, useNavigate, Outlet } from 'react-router-dom'; -import semver from 'semver'; import Loading from '@tdesign/components/loading'; -import packageJson from '../../package.json'; import * as siteConfig from '../site.config'; -import { getRoute, filterVersions } from './utils'; +import { getRoute } from './utils'; const LazyDemo = lazy(() => import('./components/Demo')); @@ -18,9 +16,6 @@ const docsMap = { en: enDocs, }; -const registryUrl = 'https://service-edbzjd6y-1257786608.hk.apigw.tencentcs.com/release/npm/versions/tdesign-react'; -const currentVersion = packageJson.version.replace(/\./g, '_'); - const docRoutes = [...getRoute(siteConfig.default.docs, []), ...getRoute(siteConfig.default.enDocs, [])]; const renderRouter = docRoutes.map((nav, i) => { const LazyCom = lazy(nav.component); diff --git a/packages/tdesign-react-aigc/site/src/main.jsx b/packages/tdesign-react-aigc/site/src/main.jsx index 80721dc442..9029fd8515 100644 --- a/packages/tdesign-react-aigc/site/src/main.jsx +++ b/packages/tdesign-react-aigc/site/src/main.jsx @@ -4,7 +4,7 @@ import { registerLocaleChange } from 'tdesign-site-components'; import App from './App'; // import tdesign style; -import '@tdesign/components/style/index.js'; +import '@tdesign/pro-components-chat/style/index.js'; import '@tdesign/common-style/web/docs.less'; import 'tdesign-site-components/lib/styles/style.css'; diff --git a/tsconfig.json b/tsconfig.json index 40f3147383..f8ddae2e1b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,9 +19,6 @@ "tdesign-react": [ "packages/components" ], - "tdesign-react/*": [ - "packages/components/*" - ], "@tdesign-react/aigc": [ "packages/pro-components/chat" ], From c67fab30421e081738c98a6d6cd77e2798c21d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 16:25:50 +0800 Subject: [PATCH 081/228] chore: build script --- package.json | 8 ++++---- packages/pro-components/chat/chatbot/useChat.ts | 3 +++ packages/tdesign-react-aigc/package.json | 9 +++------ .../rollup.config.js => script/rollup.aigc.config.js | 6 ++---- tsconfig.aigc.build.json | 10 ++++++++++ 5 files changed, 22 insertions(+), 14 deletions(-) rename internal/build/tdesign-react-aigc/rollup.config.js => script/rollup.aigc.config.js (91%) create mode 100644 tsconfig.aigc.build.json diff --git a/package.json b/package.json index 62c735d0c8..d91d2fa31b 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "site:intranet": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react/site intranet", "site:preview": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react/site preview", "dev:aigc": "pnpm -C packages/tdesign-react-aigc/site dev", - "site:aigc": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react-aigc/site build", - "site:aigc-intranet": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react-aigc/site intranet", - "site:aigc-preview": "pnpm run build:jsx-demo && pnpm -C packages/tdesign-react-aigc/site preview", + "site:aigc": "pnpm -C packages/tdesign-react-aigc/site build", + "site:aigc-intranet": "pnpm -C packages/tdesign-react-aigc/site intranet", + "site:aigc-preview": "pnpm -C packages/tdesign-react-aigc/site preview", "lint": "pnpm run lint:tsc && eslint --ext .ts,.tsx ./ --max-warnings 0", "lint:fix": "eslint --ext .ts,.tsx ./packages/components --ignore-pattern packages/components/__tests__ --max-warnings 0 --fix", "lint:tsc": "tsc -p ./tsconfig.dev.json ", @@ -29,7 +29,7 @@ "test:coverage": "vitest run --coverage", "prebuild": "rimraf packages/tdesign-react/es/* packages/tdesign-react/lib/* packages/tdesign-react/dist/* packages/tdesign-react/esm/* packages/tdesign-react/cjs/*", "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && node script/utils/bundle-override.js && pnpm run build:tsc", - "build:aigc": "cross-env NODE_ENV=production rollup -c internal/build/tdesign-react-aigc/rollup.config.js", + "build:aigc": "cross-env NODE_ENV=production rollup -c script/rollup.aigc.config.js && tsc -p ./tsconfig.aigc.build.json --outDir packages/tdesign-react-aigc/es/", "build:tsc": "run-p build:tsc-*", "build:tsc-es": "tsc -p ./tsconfig.build.json --outDir packages/tdesign-react/es/", "build:tsc-esm": "tsc -p ./tsconfig.build.json --outDir packages/tdesign-react/esm/", diff --git a/packages/pro-components/chat/chatbot/useChat.ts b/packages/pro-components/chat/chatbot/useChat.ts index c9d7746307..6b4df3e51a 100644 --- a/packages/pro-components/chat/chatbot/useChat.ts +++ b/packages/pro-components/chat/chatbot/useChat.ts @@ -3,6 +3,7 @@ import type { ChatMessagesData, ChatStatus } from 'tdesign-web-components/lib/ch import { TdChatProps } from 'tdesign-web-components'; import ChatEngine from 'tdesign-web-components/lib/chatbot/core'; +// @ts-ignore export type IUseChat = Pick; export const useChat = ({ messages: initialMessages, chatServiceConfig }: IUseChat) => { @@ -25,7 +26,9 @@ export const useChat = ({ messages: initialMessages, chatServiceConfig }: IUseCh }; const initChat = () => { + // @ts-ignore chatEngine.init(chatServiceConfig, initialMessages); + // @ts-ignore syncState(initialMessages); subscribeToChat(); }; diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index d0d0b06e23..add0ba5336 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -21,10 +21,9 @@ "start": "pnpm dev", "dev": "vite", "prebuild": "rimraf es/*", - "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && npm run build:tsc", + "build": "cross-env NODE_ENV=production rollup -c ../script/rollup.aigc.config.js && npm run build:tsc", "build:tsc": "run-p build:tsc-*", - "build:tsc-es": "tsc --emitDeclarationOnly -d -p ./tsconfig.build.json --outDir es/", - "build:jsx-demo": "npm run generate:jsx-demo && npm run format:jsx-demo" + "build:tsc-es": "tsc --emitDeclarationOnly -d -p ./tsconfig.build.json --outDir es/" }, "config": { "commitizen": { @@ -49,9 +48,7 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.0.0", - "classnames": "~2.5.1", - "lodash-es": "^4.17.21" + "tdesign-web-components": "1.0.0" }, "devDependencies": { "cors": "^2.8.5", diff --git a/internal/build/tdesign-react-aigc/rollup.config.js b/script/rollup.aigc.config.js similarity index 91% rename from internal/build/tdesign-react-aigc/rollup.config.js rename to script/rollup.aigc.config.js index 62f36d81dc..2714b41bbb 100644 --- a/internal/build/tdesign-react-aigc/rollup.config.js +++ b/script/rollup.aigc.config.js @@ -9,11 +9,9 @@ import commonjs from '@rollup/plugin-commonjs'; import { DEFAULT_EXTENSIONS } from '@babel/core'; import multiInput from 'rollup-plugin-multi-input'; import nodeResolve from '@rollup/plugin-node-resolve'; -import staticImport from 'rollup-plugin-static-import'; -import ignoreImport from 'rollup-plugin-ignore-import'; import { resolve } from 'path'; -import pkg from '../../../packages/tdesign-react-aigc/package.json'; +import pkg from '../packages/tdesign-react-aigc/package.json'; const name = 'tdesign'; const externalDeps = Object.keys(pkg.dependencies || {}); @@ -33,7 +31,7 @@ const inputList = [ '!packages/pro-components/chat/**/_usage', ]; -const getPlugins = ({ env, isProd = false, ignoreLess = true, extractMultiCss = false } = {}) => { +const getPlugins = ({ env, isProd = false } = {}) => { const plugins = [ nodeResolve({ extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'], diff --git a/tsconfig.aigc.build.json b/tsconfig.aigc.build.json new file mode 100644 index 0000000000..deb3e5b07e --- /dev/null +++ b/tsconfig.aigc.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig", + "include": ["packages/pro-components/chat"], + "exclude": ["**/**/__tests__/*", "**/**/_example/*", "**/**/_usage/*", "es", "node_modules"], + "compilerOptions": { + "emitDeclarationOnly": true, + "rootDir": "packages/pro-components/chat", + "skipLibCheck": true + } +} From 9143a166fd18e58a811d5795956adf7293943d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 16:44:41 +0800 Subject: [PATCH 082/228] chore: build script --- packages/tdesign-react-aigc/CHANGELOG.md | 10 ++++ packages/tdesign-react-aigc/LICENSE | 9 +++ packages/tdesign-react-aigc/README.md | 74 ++++++++++++++++++++++++ packages/tdesign-react-aigc/package.json | 11 ++-- 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 packages/tdesign-react-aigc/CHANGELOG.md create mode 100644 packages/tdesign-react-aigc/LICENSE create mode 100644 packages/tdesign-react-aigc/README.md diff --git a/packages/tdesign-react-aigc/CHANGELOG.md b/packages/tdesign-react-aigc/CHANGELOG.md new file mode 100644 index 0000000000..25155d2822 --- /dev/null +++ b/packages/tdesign-react-aigc/CHANGELOG.md @@ -0,0 +1,10 @@ +--- +title: 更新日志 +docClass: timeline +toc: false +spline: explain +--- + +## 🌈 0.1.0-alpha.1 `2025-05-26` + +- Release 1st version diff --git a/packages/tdesign-react-aigc/LICENSE b/packages/tdesign-react-aigc/LICENSE new file mode 100644 index 0000000000..e289dc9dad --- /dev/null +++ b/packages/tdesign-react-aigc/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025-present TDesign + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/tdesign-react-aigc/README.md b/packages/tdesign-react-aigc/README.md new file mode 100644 index 0000000000..019ef69556 --- /dev/null +++ b/packages/tdesign-react-aigc/README.md @@ -0,0 +1,74 @@ +

+ + TDesign Logo + +

+ +

+ + License + + + codecov + + + Version + + + Downloads + +

+ +TDesign AIGC Components for React Framework + +# 📦 Installation + +```shell +npm i @tdesign-react/aigc +``` + +```shell +yarn add @tdesign-react/aigc +``` + +```shell +pnpm add @tdesign-react/aigc +``` + +# 🔨 Usage + +```tsx +import React from 'react'; +import { ChatBot } from '@tdesign-react/aigc'; +import '@tdesign-react/aigc/es/style/index.js'; + +function App() { + return ; +} + +ReactDOM.createRoot(document.getElementById('app')).render(); +``` + +# Browser Support + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Edge >=84 | Firefox >=83 | Chrome >=84 | Safari >=14.1 | + +Read our [browser compatibility](https://github.com/Tencent/tdesign/wiki/Browser-Compatibility) for more details. + + +# Contributing + +Contributing is welcome. Read [guidelines for contributing](https://github.com/Tencent/tdesign-react/blob/develop/CONTRIBUTING.md) before submitting your [Pull Request](https://github.com/Tencent/tdesign-react/pulls). + + +# Feedback + +Create your [Github issues](https://github.com/Tencent/tdesign-react/issues) or scan the QR code below to join our user groups + + + +# License + +The MIT License. Please see [the license file](./LICENSE) for more information. diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index add0ba5336..7261a40543 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,6 +1,6 @@ { "name": "@tdesign-react/aigc", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", @@ -21,9 +21,10 @@ "start": "pnpm dev", "dev": "vite", "prebuild": "rimraf es/*", - "build": "cross-env NODE_ENV=production rollup -c ../script/rollup.aigc.config.js && npm run build:tsc", + "build": "cross-env NODE_ENV=production rollup -c script/rollup.config.js && npm run build:tsc", "build:tsc": "run-p build:tsc-*", - "build:tsc-es": "tsc --emitDeclarationOnly -d -p ./tsconfig.build.json --outDir es/" + "build:tsc-es": "tsc --emitDeclarationOnly -d -p ./tsconfig.build.json --outDir es/", + "build:jsx-demo": "npm run generate:jsx-demo && npm run format:jsx-demo" }, "config": { "commitizen": { @@ -48,7 +49,9 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.0.0" + "tdesign-web-components": "1.0.0", + "classnames": "~2.5.1", + "lodash-es": "^4.17.21" }, "devDependencies": { "cors": "^2.8.5", From 1b5ced303f677f5cbd26470210729d3d55ff11df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 17:03:27 +0800 Subject: [PATCH 083/228] feat: add typing --- packages/tdesign-react-aigc/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 7261a40543..734d34b506 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,9 +1,10 @@ { "name": "@tdesign-react/aigc", - "version": "0.1.0-alpha.1", + "version": "0.1.0-alpha.2", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", + "typings": "es/index.d.ts", "files": [ "es", "LICENSE", From 8b57f419383b69e6918b34983ee2deb92f78a279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 17:13:42 +0800 Subject: [PATCH 084/228] chore: update docs --- .../site/docs/getting-started.md | 2 +- .../site/plugin-tdoc/md-to-react.js | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/tdesign-react-aigc/site/docs/getting-started.md b/packages/tdesign-react-aigc/site/docs/getting-started.md index 462070ad0c..f707280ba9 100644 --- a/packages/tdesign-react-aigc/site/docs/getting-started.md +++ b/packages/tdesign-react-aigc/site/docs/getting-started.md @@ -22,7 +22,7 @@ npm i @tdesign-react/aigc ```javascript import { ChatBot } from '@tdesign-react/aigc'; -import '@tdesign-react/aigc/es/style/index.css'; // 少量公共样式 +import '@tdesign-react/aigc/es/style/index.js'; // 少量公共样式 ``` ## 浏览器兼容性 diff --git a/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js b/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js index f2c3ffbeeb..5b5acf9b92 100644 --- a/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js +++ b/packages/tdesign-react-aigc/site/plugin-tdoc/md-to-react.js @@ -95,23 +95,6 @@ export default async function mdToReact(options) { spline="${mdSegment.spline}" platform="web" > - ${ - mdSegment.isComponent - ? ` - - - - ` - : '' - } ` : '' } From 3650862366f574eef0daccc790df4abe6afc8494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 17:25:27 +0800 Subject: [PATCH 085/228] chore: update docs --- packages/tdesign-react-aigc/site/vite.config.js | 2 -- tsconfig.json | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js index e78c312e41..6e2110c917 100644 --- a/packages/tdesign-react-aigc/site/vite.config.js +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -27,8 +27,6 @@ export default ({ mode }) => alias: { '@tdesign-react/aigc': path.resolve(__dirname, '../../pro-components/chat'), '@tdesign/react-aigc-site': path.resolve(__dirname, './'), - 'tdesign-react': path.resolve(__dirname, '../../components'), - 'tdesign-react/es': path.resolve(__dirname, '../../tdesign-react'), }, }, build: { diff --git a/tsconfig.json b/tsconfig.json index f8ddae2e1b..44b5002ecf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,9 +19,15 @@ "tdesign-react": [ "packages/components" ], + "tdesign-react/*": [ + "packages/components/*" + ], "@tdesign-react/aigc": [ "packages/pro-components/chat" ], + "@tdesign-react/aigc/*": [ + "packages/pro-components/chat/*" + ], "@test/utils": [ "test/utils" ], From 395cbe46e052f6a70f979e385326d4075f3cadf3 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 26 May 2025 17:48:22 +0800 Subject: [PATCH 086/228] feat(chatbot): add export type --- packages/pro-components/chat/chat-message/index.ts | 2 ++ packages/pro-components/chat/chatbot/index.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/pro-components/chat/chat-message/index.ts b/packages/pro-components/chat/chat-message/index.ts index 605cf6a69d..4d44688639 100644 --- a/packages/pro-components/chat/chat-message/index.ts +++ b/packages/pro-components/chat/chat-message/index.ts @@ -7,3 +7,5 @@ export const ChatMessage: React.ForwardRefExoticComponent< > = reactify('t-chat-item'); export default ChatMessage; + +export type { TdChatItemProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chatbot/index.ts b/packages/pro-components/chat/chatbot/index.ts index 8934188638..064109db2c 100644 --- a/packages/pro-components/chat/chatbot/index.ts +++ b/packages/pro-components/chat/chatbot/index.ts @@ -10,8 +10,6 @@ import type { } from 'tdesign-web-components'; import reactify from '../_util/reactify'; -export * from 'tdesign-web-components/lib/chatbot/core/utils'; - export * from './useChat'; const ChatBot: React.ForwardRefExoticComponent< @@ -31,4 +29,6 @@ const ChatList: React.ForwardRefExoticComponent< > = reactify('t-chat-list'); export { ChatBot, ChatSearchContent, ChatSuggestionContent, ChatList }; -// export type * from 'tdesign-web-components/lib/chatbot/type'; + +export * from 'tdesign-web-components/lib/chatbot/core/utils'; +export type * from 'tdesign-web-components/lib/chatbot/type'; From 0299f6988650a052b72553fe2949c34e01946961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E6=B5=B7=E7=8F=8A?= Date: Mon, 26 May 2025 19:28:54 +0800 Subject: [PATCH 087/228] feat: live demo --- .../chat/chat-message/_usage/index.jsx | 76 +++++++++++++++++++ .../chat/chat-message/_usage/props.json | 59 ++++++++++++++ .../chat/chat-thinking/_usage/index.jsx | 57 ++++++++++++++ .../chat/chat-thinking/_usage/props.json | 57 ++++++++++++++ .../chat/chat-thinking/chat-thinking.md | 2 +- 5 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 packages/pro-components/chat/chat-message/_usage/index.jsx create mode 100644 packages/pro-components/chat/chat-message/_usage/props.json create mode 100644 packages/pro-components/chat/chat-thinking/_usage/index.jsx create mode 100644 packages/pro-components/chat/chat-thinking/_usage/props.json diff --git a/packages/pro-components/chat/chat-message/_usage/index.jsx b/packages/pro-components/chat/chat-message/_usage/index.jsx new file mode 100644 index 0000000000..1b59342b87 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_usage/index.jsx @@ -0,0 +1,76 @@ +/** + * 该脚本为自动生成,如有需要请在 /script/generate-usage.js 中调整 + */ + +// @ts-nocheck +import React, { useState, useEffect, useMemo } from 'react'; +import BaseUsage, { useConfigChange, usePanelChange } from '@tdesign/react-site/src/components/BaseUsage'; +import jsxToString from 'react-element-to-jsx-string'; + +import { ChatMessage } from '@tdesign-react/aigc'; + +import configProps from './props.json'; + +const message = { + content: [ + { + type: 'text', + data: '牛顿第一定律是否适用于所有参考系?', + }, + { + id: '11111', + role: 'assistant', + status: 'pending', + } + ], +}; + +export default function Usage() { + const [configList, setConfigList] = useState(configProps); + + const { changedProps, onConfigChange } = useConfigChange(configList); + + const panelList = [{ label: 'ChatMessage', value: 'ChatMessage' }]; + + const { panel, onPanelChange } = usePanelChange(panelList); + + const [renderComp, setRenderComp] = useState(); + + useEffect(() => { + setRenderComp( +
+ + +
, + ); + }, [changedProps]); + + const jsxStr = useMemo(() => { + if (!renderComp) return ''; + return jsxToString(renderComp); + }, [renderComp]); + + return ( + + {renderComp} + + ); +} diff --git a/packages/pro-components/chat/chat-message/_usage/props.json b/packages/pro-components/chat/chat-message/_usage/props.json new file mode 100644 index 0000000000..6d714525e7 --- /dev/null +++ b/packages/pro-components/chat/chat-message/_usage/props.json @@ -0,0 +1,59 @@ +[ + { + "name": "variant", + "type": "enum", + "defaultValue": "base", + "options": [ + { + "label": "base", + "value": "base" + }, + { + "label": "outline", + "value": "outline" + }, + { + "label": "text", + "value": "text" + } + ] + }, + { + "name": "animation", + "type": "enum", + "defaultValue": "skeleton", + "options": [ + { + "label": "skeleton", + "value": "skeleton" + }, + { + "label": "moving", + "value": "moving" + }, + { + "label": "gradient", + "value": "gradient" + }, + { + "label": "circle", + "value": "circle" + } + ] + }, + { + "name": "placement", + "type": "enum", + "defaultValue": "left", + "options": [ + { + "label": "left", + "value": "left" + }, + { + "label": "right", + "value": "right" + } + ] + } +] \ No newline at end of file diff --git a/packages/pro-components/chat/chat-thinking/_usage/index.jsx b/packages/pro-components/chat/chat-thinking/_usage/index.jsx new file mode 100644 index 0000000000..e6ebf26aa9 --- /dev/null +++ b/packages/pro-components/chat/chat-thinking/_usage/index.jsx @@ -0,0 +1,57 @@ +/** + * 该脚本为自动生成,如有需要请在 /script/generate-usage.js 中调整 + */ + +// @ts-nocheck +import React, { useState, useEffect, useMemo } from 'react'; +import BaseUsage, { useConfigChange, usePanelChange } from '@tdesign/react-site/src/components/BaseUsage'; +import jsxToString from 'react-element-to-jsx-string'; + +import { ChatThinking } from '@tdesign-react/aigc'; + +import configProps from './props.json'; + +const fullText = + '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。'; + +export default function Usage() { + const [configList, setConfigList] = useState(configProps); + + const { changedProps, onConfigChange } = useConfigChange(configList); + + const panelList = [{ label: 'ChatMessage', value: 'ChatMessage' }]; + + const { panel, onPanelChange } = usePanelChange(panelList); + + const [renderComp, setRenderComp] = useState(); + + + useEffect(() => { + setRenderComp( +
+ +
, + ); + }, [changedProps]); + + const jsxStr = useMemo(() => { + if (!renderComp) return ''; + return jsxToString(renderComp); + }, [renderComp]); + + return ( + + {renderComp} + + ); +} diff --git a/packages/pro-components/chat/chat-thinking/_usage/props.json b/packages/pro-components/chat/chat-thinking/_usage/props.json new file mode 100644 index 0000000000..563662307d --- /dev/null +++ b/packages/pro-components/chat/chat-thinking/_usage/props.json @@ -0,0 +1,57 @@ +[ + { + "name": "collapsed", + "type": "Boolean", + "defaultValue": false, + "options": [] + }, + { + "name": "layout", + "type": "enum", + "defaultValue": "border", + "options": [ + { + "label": "border", + "value": "border" + }, + { + "label": "block", + "value": "block" + } + ] + }, + { + "name": "animation", + "type": "enum", + "defaultValue": "moving", + "options": [ + { + "label": "moving", + "value": "moving" + }, + { + "label": "gradient", + "value": "gradient" + }, + { + "label": "circle", + "value": "circle" + } + ] + }, + { + "name": "status", + "type": "enum", + "defaultValue": "pending", + "options": [ + { + "label": "pending", + "value": "pending" + }, + { + "label": "complete", + "value": "complete" + } + ] + } +] \ No newline at end of file diff --git a/packages/pro-components/chat/chat-thinking/chat-thinking.md b/packages/pro-components/chat/chat-thinking/chat-thinking.md index ec8f705311..22cf9582f1 100644 --- a/packages/pro-components/chat/chat-thinking/chat-thinking.md +++ b/packages/pro-components/chat/chat-thinking/chat-thinking.md @@ -3,7 +3,7 @@ title: ChatThinking 思考过程 description: 思考过程 isComponent: true usage: { title: '', description: '' } -spline: navigation +spline: aigc --- ## 基础用法 From 6456c6a5bb715a932b295a0915b4927df4234981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Mon, 26 May 2025 19:58:42 +0800 Subject: [PATCH 088/228] chore: update vite config --- packages/tdesign-react-aigc/site/vite.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tdesign-react-aigc/site/vite.config.js b/packages/tdesign-react-aigc/site/vite.config.js index 6e2110c917..9ba4a9c140 100644 --- a/packages/tdesign-react-aigc/site/vite.config.js +++ b/packages/tdesign-react-aigc/site/vite.config.js @@ -27,6 +27,8 @@ export default ({ mode }) => alias: { '@tdesign-react/aigc': path.resolve(__dirname, '../../pro-components/chat'), '@tdesign/react-aigc-site': path.resolve(__dirname, './'), + 'tdesign-react/es': path.resolve(__dirname, '../../components'), + 'tdesign-react': path.resolve(__dirname, '../../components'), }, }, build: { From 9515b7a139a8cb2762ad2eceb803fb988e79671b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E6=B5=B7=E7=8F=8A?= Date: Tue, 27 May 2025 15:41:04 +0800 Subject: [PATCH 089/228] feat: api --- .../chat-actionbar/chat-actionbar.en-US.md | 26 ++----- .../chat/chat-actionbar/chat-actionbar.md | 24 ++----- .../chat-attachments.en-US.md | 33 ++++----- .../chat/chat-attachments/chat-attachments.md | 32 ++++----- .../chat/chat-filecard/chat-filecard.en-US.md | 27 ++----- .../chat/chat-filecard/chat-filecard.md | 26 ++----- .../chat/chat-loading/chat-loading.en-US.md | 23 +----- .../chat/chat-loading/chat-loading.md | 23 +----- .../chat/chat-markdown/chat-markdown.en-US.md | 26 ++----- .../chat/chat-markdown/chat-markdown.md | 23 ++---- .../chat/chat-message/chat-message.en-US.md | 70 +++++++++++++------ .../chat/chat-message/chat-message.md | 68 ++++++++++++------ .../chat/chat-sender/chat-sender.en-US.md | 49 +++++++------ .../chat/chat-sender/chat-sender.md | 49 +++++++------ .../chat/chat-thinking/chat-thinking.en-US.md | 28 +++----- .../chat/chat-thinking/chat-thinking.md | 27 ++----- .../chat/chatbot/chatbot.en-US.md | 53 ++++++++------ .../pro-components/chat/chatbot/chatbot.md | 51 +++++++++----- 18 files changed, 301 insertions(+), 357 deletions(-) diff --git a/packages/pro-components/chat/chat-actionbar/chat-actionbar.en-US.md b/packages/pro-components/chat/chat-actionbar/chat-actionbar.en-US.md index c417d90bbb..04cdc8e9a3 100644 --- a/packages/pro-components/chat/chat-actionbar/chat-actionbar.en-US.md +++ b/packages/pro-components/chat/chat-actionbar/chat-actionbar.en-US.md @@ -1,26 +1,12 @@ :: BASE_DOC :: ## API -### Button Props +### ChatActionBar Props name | type | default | description | required -- | -- | -- | -- | -- -className | String | - | 类名 | N -style | Object | - | 样式,Typescript:`React.CSSProperties` | N -block | Boolean | false | make button to be a block-level element | N -children | TNode | - | button's children elements。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -content | TNode | - | button's children elements。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -disabled | Boolean | false | disable the button, make it can not be clicked | N -form | String | undefined | native `form` attribute,which supports triggering events for a form with a specified id through the use of the form attribute. | N -ghost | Boolean | false | make background-color to be transparent | N -href | String | - | \- | N -icon | TElement | - | use it to set left icon in button。Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -loading | Boolean | false | set button to be loading state | N -shape | String | rectangle | button shape。options:rectangle/square/round/circle | N -size | String | medium | a button has three size。options:small/medium/large。Typescript:`SizeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -suffix | TElement | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -tag | String | - | HTML Tag Element。options:button/a/div | N -theme | String | - | button theme。options:default/primary/danger/warning/success | N -type | String | button | type of button element in html。options:submit/reset/button | N -variant | String | base | variant of button。options:base/outline/dashed/text | N -onClick | Function | | Typescript:`(e: MouseEvent) => void`
trigger on click | N +actionBar | Array / Boolean | true | 操作按钮配置项,可配置操作按钮选项和顺序。数组可选项:replay/copy/good/bad/goodActived/badActived/share | N +onActions | Function | - | 操作按钮回调函数。TS类型:`Record void>` | N +presetActions | Array | - | 预制按钮。TS类型:`Record<{name: TdChatItemActionName, render: TNode, condition?: (message: ChatMessagesData) => boolean;}>` | N +message | Object | - | 对话数据信息 | N +tooltipProps | TooltipProps | - | [类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/tooltip/type.ts) | N diff --git a/packages/pro-components/chat/chat-actionbar/chat-actionbar.md b/packages/pro-components/chat/chat-actionbar/chat-actionbar.md index 9fff41a19a..d9055d7f54 100644 --- a/packages/pro-components/chat/chat-actionbar/chat-actionbar.md +++ b/packages/pro-components/chat/chat-actionbar/chat-actionbar.md @@ -29,22 +29,8 @@ spline: aigc 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- -className | String | - | 类名 | N -style | Object | - | 样式,TS 类型:`React.CSSProperties` | N -block | Boolean | false | 是否为块级元素 | N -children | TNode | - | 按钮内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -content | TNode | - | 按钮内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -disabled | Boolean | false | 禁用状态 | N -form | String | undefined | 原生的form属性,支持用于通过 form 属性触发对应 id 的 form 的表单事件 | N -ghost | Boolean | false | 是否为幽灵按钮(镂空按钮) | N -href | String | - | 跳转地址。href 存在时,按钮标签默认使用 `` 渲染;如果指定了 `tag` 则使用指定的标签渲染 | N -icon | TElement | - | 按钮内部图标,可完全自定义。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -loading | Boolean | false | 是否显示为加载状态 | N -shape | String | rectangle | 按钮形状,有 4 种:长方形、正方形、圆角长方形、圆形。可选项:rectangle/square/round/circle | N -size | String | medium | 组件尺寸。可选项:small/medium/large。TS 类型:`SizeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -suffix | TElement | - | 右侧内容,可用于定义右侧图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -tag | String | - | 渲染按钮的 HTML 标签,默认使用标签 `
{/* 自定义输入框左侧区域slot,可以用来触发工具场景切换 */} -
+
{options.filter((item) => item.value === scene)[0].content} diff --git a/packages/pro-components/chat/chat-sender/chat-sender.en-US.md b/packages/pro-components/chat/chat-sender/chat-sender.en-US.md index fa8dde2c92..1e0153ab5b 100644 --- a/packages/pro-components/chat/chat-sender/chat-sender.en-US.md +++ b/packages/pro-components/chat/chat-sender/chat-sender.en-US.md @@ -31,5 +31,5 @@ onAction | Function | - | 操作按钮点击事件。参数:`{ action: string, | header | 顶部自定义内容 | | inner-header | 输入区域顶部内容 | | prefix | 输入框前缀内容 | -| footer-left | 底部左侧区域 | +| footer-prefix | 底部左侧区域 | | actions | 操作按钮区域 | diff --git a/packages/pro-components/chat/chat-sender/chat-sender.md b/packages/pro-components/chat/chat-sender/chat-sender.md index 3c297c5076..7bda48c794 100644 --- a/packages/pro-components/chat/chat-sender/chat-sender.md +++ b/packages/pro-components/chat/chat-sender/chat-sender.md @@ -20,7 +20,7 @@ spline: navigation ## 自定义 通过植入具名插槽来实现输入框的自定义,内置支持的扩展位置包括: -输入框上方区域`header`,输入框内头部区域`inner-header`,可输入区域前置部分`prefix`,输入框底部左侧区域`footer-left`,输入框底部操作区域`actions` +输入框上方区域`header`,输入框内头部区域`inner-header`,可输入区域前置部分`prefix`,输入框底部左侧区域`footer-prefix`,输入框底部操作区域`actions` 同时示例中演示了通过`CSS变量覆盖`实现样式定制 @@ -56,6 +56,6 @@ onAction | Function | - | 操作按钮点击事件。参数:`{ action: string, |--------|------| | header | 顶部自定义内容 | | inner-header | 输入区域顶部内容 | -| prefix | 输入框前缀内容 | -| footer-left | 底部左侧区域 | +| input-prefix | 输入框前缀内容 | +| footer-prefix | 底部左侧区域 | | actions | 操作按钮区域 | \ No newline at end of file diff --git a/packages/pro-components/chat/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx index 37546d1a8e..6ce7b78e42 100644 --- a/packages/pro-components/chat/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -185,7 +185,7 @@ export default function chatSample() { chatServiceConfig={chatServiceConfig} > {/* 自定义输入框底部区域slot,可以增加模型选项 */} -
+
diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index 6ce4f88b89..33717fbaa9 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -40,6 +40,35 @@ const ChartDemo = ({ data }) => ( ); const initMessage: ChatMessagesData[] = [ + { + id: '7389', + role: 'user', + status: 'complete', + content: [ + { + type: 'attachment', + data: [ + { + fileType: 'image', + // name: '', + // size: 234234, + // extension: '.doc', + url: 'https://tdesign.gtimg.com/site/avatar.jpg', + }, + { + fileType: 'image', + // name: 'avatar.jpg', + // size: 234234, + url: 'https://asset.gdtimg.com/muse_svp_0bc3viaacaaaweanalstw5ud3kweagvaaaka.f0.jpg?dis_k=bfc5cc81010a9d443e91ce45d4fbe774&dis_t=1750323484', + }, + ], + }, + { + type: 'text', + data: '这张图里的帅哥是谁', + }, + ], + }, { id: '123', role: 'assistant', @@ -159,6 +188,13 @@ export default function ChatBotReact() {
); + case 'videoAttachment': { + return ( +
+ +
+ ); + } } return null; }), diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index 5dd98fee89..92305c1142 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -66,9 +66,9 @@ messageProps | Object/Function | - | 消息项配置。按角色聚合了消息 listProps | Object | - | 消息列表配置。TS类型:`TdChatListProps`。 | N senderProps | Object | - | 发送框配置,透传`ChatSender`组件。TS类型:`TdChatSenderProps`。[类型定义](./chat-sender?tab=api) | N chatServiceConfig | Object | - | 聊天服务配置,见下方详细说明,TS类型:`ChatServiceConfig` | N -onMessageChange | Function | - | 消息变化回调,TS类型:`(e: CustomEvent) => void` | N +onMessageChange | Function | - | 消息列表数据变化回调,TS类型:`(e: CustomEvent) => void` | N onChatReady | Function | - | 内部消息引擎初始化完成回调,TS类型:`(e: CustomEvent) => void` | N - +onChatSent | Function | - | 发送消息回调,TS类型:`(e: CustomEvent) => void` | N ### TdChatListProps 消息列表配置 @@ -106,7 +106,7 @@ addPrompt | (prompt: string) => void | 将预设提示语添加到输入框, selectFile | () => void | 触发文件选择对话框,用于附件上传功能 regenerate | (keepVersion?: boolean) => Promise | 重新生成最后一条消息,可选保留历史版本 registerMergeStrategy | (type: T['type'], handler: (chunk: T, existing?: T) => T) => void | 注册自定义消息合并策略,用于处理流式数据更新 -scrollToBottom | () => void | 将消息列表滚动到底部,适用于有新消息时自动定位 +scrollList | ({ to: 'bottom' \| 'top', behavior: 'auto' \| 'smooth' }) => void | 受控滚动到指定位置 isChatEngineReady | boolean | ChatEngine是否就绪 chatMessageValue | ChatMessagesData[] | 获取当前消息列表的只读副本 chatStatus | ChatStatus | 获取当前聊天状态(空闲/进行中/错误等) @@ -128,6 +128,6 @@ chatServiceConfig | ChatServiceConfigSetter | 聊天服务配置,支持静态 返回值 | 类型 | 说明 -- | -- | -- -chatEngine | ChatEngine 实例 | 聊天引擎实例,提供核心操作方法,同上方 `Chatbot 实例方法` +chatEngine | IChatEngine | 聊天引擎实例,提供核心操作方法,同上方 `Chatbot 实例方法` messages | ChatMessagesData[] | 当前聊天消息列表所有数据 status | ChatStatus | 当前聊天状态 diff --git a/packages/pro-components/chat/chatbot/useChat.ts b/packages/pro-components/chat/chatbot/useChat.ts index d2f3037a4c..4e4f3eecb5 100644 --- a/packages/pro-components/chat/chatbot/useChat.ts +++ b/packages/pro-components/chat/chatbot/useChat.ts @@ -5,6 +5,7 @@ import ChatEngine from 'tdesign-web-components/lib/chatbot/core'; // @ts-ignore export type IUseChat = Pick; +export type IChatEngine = typeof ChatEngine; export const useChat = ({ defaultMessages: initialMessages, chatServiceConfig }: IUseChat) => { const [messages, setMessage] = useState([]); diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 3a4c4773d5..6afc5cd36c 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,6 +1,6 @@ { "name": "@tdesign-react/aigc", - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.11", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", @@ -50,7 +50,7 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.1.1", + "tdesign-web-components": "1.1.3", "classnames": "~2.5.1", "lodash-es": "^4.17.21" }, From a681be0619e45dd3b7e3281ecdefd1f3bee2401b Mon Sep 17 00:00:00 2001 From: brightzhli <864345220@qq.com> Date: Thu, 26 Jun 2025 14:42:00 +0800 Subject: [PATCH 113/228] fix(example): chat-message base example --- packages/pro-components/chat/chat-message/_example/base.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pro-components/chat/chat-message/_example/base.tsx b/packages/pro-components/chat/chat-message/_example/base.tsx index d38d818877..dd4dc442e0 100644 --- a/packages/pro-components/chat/chat-message/_example/base.tsx +++ b/packages/pro-components/chat/chat-message/_example/base.tsx @@ -3,6 +3,8 @@ import { Space } from 'tdesign-react'; import { UserMessage, ChatMessage } from '@tdesign-react/aigc'; const message: UserMessage = { + id: '1', + role: 'user', content: [ { type: 'text', From ade7067042dff979ffef3aed63dc6840f765717e Mon Sep 17 00:00:00 2001 From: carolin913 Date: Wed, 2 Jul 2025 19:29:43 +0800 Subject: [PATCH 114/228] feat(chatsender): new style --- .../pro-components/chat/chat-sender/_example/attachment.tsx | 4 ++-- .../pro-components/chat/chat-sender/_example/custom.tsx | 3 +-- packages/pro-components/chat/chatbot/_example/custom.tsx | 2 +- packages/pro-components/chat/chatbot/_example/docs.tsx | 6 +++--- .../pro-components/chat/chatbot/_example/hookComponent.tsx | 2 +- packages/tdesign-react-aigc/package.json | 4 ++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/pro-components/chat/chat-sender/_example/attachment.tsx b/packages/pro-components/chat/chat-sender/_example/attachment.tsx index e474021ed8..b099658269 100644 --- a/packages/pro-components/chat/chat-sender/_example/attachment.tsx +++ b/packages/pro-components/chat/chat-sender/_example/attachment.tsx @@ -46,7 +46,7 @@ const ChatSenderExample = () => { setFiles(e.detail); }; - const onAttachmentsSelect = (e: CustomEvent) => { + const onAttachmentsSelect = (e: CustomEvent) => { // 添加新文件并模拟上传进度 const newFile = { ...e.detail[0], @@ -65,7 +65,7 @@ const ChatSenderExample = () => { ...file, url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'success', - description: `${Math.floor(newFile.size / 1024)}KB`, + description: `${Math.floor((newFile?.size || 0) / 1024)}KB`, } : file, ), diff --git a/packages/pro-components/chat/chat-sender/_example/custom.tsx b/packages/pro-components/chat/chat-sender/_example/custom.tsx index cc83cb808c..5772b9d832 100644 --- a/packages/pro-components/chat/chat-sender/_example/custom.tsx +++ b/packages/pro-components/chat/chat-sender/_example/custom.tsx @@ -36,8 +36,7 @@ const ChatSenderExample = () => { // 这里是为了演示样式修改不影响其他Demo,实际项目中直接设置css变量到:root即可 useDynamicStyle(senderRef, { '--td-text-color-placeholder': '#DFE2E7', - '--td-bg-color-secondarycontainer': '#fff', - '--td-chat-input-background': ' #fff', + '--td-chat-input-radius': '6px', }); // 输入变化处理 diff --git a/packages/pro-components/chat/chatbot/_example/custom.tsx b/packages/pro-components/chat/chatbot/_example/custom.tsx index 33717fbaa9..991644a259 100644 --- a/packages/pro-components/chat/chatbot/_example/custom.tsx +++ b/packages/pro-components/chat/chatbot/_example/custom.tsx @@ -59,7 +59,7 @@ const initMessage: ChatMessagesData[] = [ fileType: 'image', // name: 'avatar.jpg', // size: 234234, - url: 'https://asset.gdtimg.com/muse_svp_0bc3viaacaaaweanalstw5ud3kweagvaaaka.f0.jpg?dis_k=bfc5cc81010a9d443e91ce45d4fbe774&dis_t=1750323484', + url: 'https://avatars.githubusercontent.com/Jayclelon', }, ], }, diff --git a/packages/pro-components/chat/chatbot/_example/docs.tsx b/packages/pro-components/chat/chatbot/_example/docs.tsx index c2a1ab90ab..cc27e54282 100644 --- a/packages/pro-components/chat/chatbot/_example/docs.tsx +++ b/packages/pro-components/chat/chatbot/_example/docs.tsx @@ -109,7 +109,7 @@ export default function chatSample() { }; // 文件上传 - const onFileSelect = (e: CustomEvent) => { + const onFileSelect = (e: CustomEvent) => { // 添加新文件并模拟上传进度 const newFile = { ...e.detail[0], @@ -137,7 +137,7 @@ export default function chatSample() { }; // 移除文件回调 - const onFileRemove = (e: CustomEvent) => { + const onFileRemove = (e: CustomEvent) => { setFiles(e.detail); }; @@ -162,10 +162,10 @@ export default function chatSample() { items: files, overflow: 'scrollX', }, - onSend, onFileSelect, onFileRemove, }} + onChatSent={onSend} chatServiceConfig={chatServiceConfig} >
diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 5e1cb68365..7911805582 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -150,7 +150,7 @@ export default function ComponentsBuild() { const sendUserMessage = async (requestParams: ChatRequestParams) => { await chatEngine.sendUserMessage(requestParams); - listRef.current?.scrollToBottom(); + listRef.current?.scrollList({ to: 'bottom' }); }; const inputChangeHandler = (e: CustomEvent) => { diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 6afc5cd36c..495dd8e88c 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,6 +1,6 @@ { "name": "@tdesign-react/aigc", - "version": "0.1.0-alpha.11", + "version": "0.1.0-alpha.12", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", @@ -50,7 +50,7 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.1.3", + "tdesign-web-components": "1.1.5", "classnames": "~2.5.1", "lodash-es": "^4.17.21" }, From d3c65e6f4ad41df991da1f6c8b2d4b9e3094d426 Mon Sep 17 00:00:00 2001 From: lincao Date: Mon, 21 Jul 2025 08:43:22 +0800 Subject: [PATCH 115/228] feat(chatbot): add agui adapter --- .../chat/chatbot/_example/agui-step.tsx | 300 ++++++++++ .../chat/chatbot/_example/agui.tsx | 163 +++++ .../chat/chatbot/_example/hookComponent.tsx | 2 +- .../core/adapters/AGUI_ADAPTER_README.md | 261 ++++++++ .../chat/chatbot/core/adapters/README.md | 454 ++++++++++++++ .../chatbot/core/adapters/agui-adapter.ts | 561 ++++++++++++++++++ .../core/adapters/agui/agui-event-mapper.ts | 159 +++++ .../chat/chatbot/core/adapters/agui/events.ts | 226 +++++++ .../chat/chatbot/core/adapters/agui/index.ts | 224 +++++++ .../chat/chatbot/core/adapters/agui/types.ts | 106 ++++ .../core/enhanced-server/batch-client.ts | 63 ++ .../enhanced-server/connection-manager.ts | 88 +++ .../chatbot/core/enhanced-server/errors.ts | 50 ++ .../chatbot/core/enhanced-server/index.ts | 22 + .../core/enhanced-server/llm-service.ts | 140 +++++ .../core/enhanced-server/sse-client.ts | 295 +++++++++ .../core/enhanced-server/sse-parser.ts | 149 +++++ .../chatbot/core/enhanced-server/types.ts | 78 +++ .../pro-components/chat/chatbot/core/index.ts | 309 ++++++++++ .../chat/chatbot/core/processor/index.ts | 157 +++++ .../chat/chatbot/core/store/message.ts | 234 ++++++++ .../chat/chatbot/core/store/model.ts | 32 + .../chat/chatbot/core/store/reactiveState.ts | 146 +++++ .../pro-components/chat/chatbot/core/type.ts | 341 +++++++++++ .../chat/chatbot/core/utils/eventEmitter.ts | 46 ++ .../chat/chatbot/core/utils/index.ts | 89 +++ .../chat/chatbot/core/utils/logger.ts | 69 +++ .../pro-components/chat/chatbot/useChat.ts | 4 +- packages/tdesign-react-aigc/package.json | 3 +- 29 files changed, 4767 insertions(+), 4 deletions(-) create mode 100644 packages/pro-components/chat/chatbot/_example/agui-step.tsx create mode 100644 packages/pro-components/chat/chatbot/_example/agui.tsx create mode 100644 packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md create mode 100644 packages/pro-components/chat/chatbot/core/adapters/README.md create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui/events.ts create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui/index.ts create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui/types.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/batch-client.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/connection-manager.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/errors.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/index.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/llm-service.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/sse-client.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/sse-parser.ts create mode 100644 packages/pro-components/chat/chatbot/core/enhanced-server/types.ts create mode 100644 packages/pro-components/chat/chatbot/core/index.ts create mode 100644 packages/pro-components/chat/chatbot/core/processor/index.ts create mode 100644 packages/pro-components/chat/chatbot/core/store/message.ts create mode 100644 packages/pro-components/chat/chatbot/core/store/model.ts create mode 100644 packages/pro-components/chat/chatbot/core/store/reactiveState.ts create mode 100644 packages/pro-components/chat/chatbot/core/type.ts create mode 100644 packages/pro-components/chat/chatbot/core/utils/eventEmitter.ts create mode 100644 packages/pro-components/chat/chatbot/core/utils/index.ts create mode 100644 packages/pro-components/chat/chatbot/core/utils/logger.ts diff --git a/packages/pro-components/chat/chatbot/_example/agui-step.tsx b/packages/pro-components/chat/chatbot/_example/agui-step.tsx new file mode 100644 index 0000000000..920a689935 --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/agui-step.tsx @@ -0,0 +1,300 @@ +import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import { + type SSEChunkData, + type TdChatMessageConfig, + type AIMessageContent, + type ChatRequestParams, + type ChatMessagesData, + type ChatBaseContent, + ChatList, + ChatSender, + ChatMessage, + TdChatListApi, + TdChatSenderApi, + ChatActionBar, + isAIMessage, + useChat, +} from '@tdesign-react/aigc'; +import { getMessageContentForCopy, TdChatActionsName, TdChatSenderParams } from 'tdesign-web-components'; +import { Timeline } from 'tdesign-react'; +import { CheckCircleFilledIcon } from 'tdesign-icons-react'; +import mockData from './mock/data'; + +import './index.css'; + +const AgentTimeline = ({ steps }) => ( +
+ + {steps.map((step) => ( + } + > +
+
{step.step}
+ {step?.tasks?.map((task, taskIndex) => ( +
+
{task.text}
+
+ ))} +
+
+ ))} +
+
+); + +// 扩展自定义消息体类型 +declare module '@tdesign-react/aigc' { + interface AIContentTypeOverrides { + agent: ChatBaseContent< + 'agent', + { + id: string; + state: 'pending' | 'command' | 'result' | 'finish'; + content: { + steps?: { + step: string; + agent_id: string; + status: string; + tasks?: { + type: 'command' | 'result'; + text: string; + }[]; + }[]; + text?: string; + }; + } + >; + } +} + +export default function ComponentsBuild() { + const listRef = useRef(null); + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState('南极的自动提款机叫什么名字'); + const { chatEngine, messages, status } = useChat({ + defaultMessages: mockData.normal, + // 聊天服务配置 + chatServiceConfig: { + // 对话服务地址 + endpoint: `http://127.0.0.1:3000/sse/agui`, + // endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/agent`, + protocol: 'agui', + stream: true, + // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) + onComplete: (aborted: boolean, params: RequestInit) => { + console.log('onComplete', aborted, params); + }, + // 流式对话过程中出错业务自定义行为 + onError: (err: Error | Response) => { + console.error('Chatservice Error:', err); + }, + // 流式对话过程中用户主动结束对话业务自定义行为 + onAbort: async () => {}, + onMessage: (chunk: SSEChunkData): AIMessageContent => { + const { type, ...rest } = chunk.data; + switch (type) { + // 正文 + case 'text': + return { + type: 'markdown', + data: rest?.msg || '', + }; + case 'agent': + return { + type: 'agent', + ...rest, + }; + default: + return { + ...chunk.data, + data: { ...chunk.data.content }, + }; + } + }, + // 自定义请求参数 + onRequest: (innerParams: ChatRequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'agent_uid', + prompt, + }), + }; + }, + }, + }); + + const senderLoading = useMemo(() => { + if (status === 'pending' || status === 'streaming') { + return true; + } + return false; + }, [status]); + + // 消息属性配置 + const messageProps: TdChatMessageConfig = { + user: { + variant: 'base', + placement: 'right', + }, + assistant: { + placement: 'left', + // 内置的消息渲染配置 + chatContentProps: { + thinking: { + maxHeight: 100, + }, + }, + }, + }; + + const getChatActionBar = (isLast: boolean) => { + let filterActions = ['replay', 'good', 'bad', 'copy']; + if (!isLast) { + // 只有最后一条AI消息才能重新生成 + filterActions = filterActions.filter((item) => item !== 'replay'); + } + return filterActions; + }; + + const actionHandler = (name: string, data?: any) => { + switch (name) { + case 'replay': { + console.log('自定义重新回复'); + chatEngine.regenerateAIMessage(); + return; + } + default: + console.log('触发action', name, 'data', data); + } + }; + + /** 渲染消息内容体 */ + const renderMsgContents = (message: ChatMessagesData, isLast: boolean): ReactNode => ( + <> + {message.content.map((item, index) => { + if (item.type === 'agent') { + return ( +
+ +
+ ); + } + return null; + })} + {isAIMessage(message) && message.status === 'complete' ? ( + + ) : null} + + ); + + const sendUserMessage = async (requestParams: ChatRequestParams) => { + await chatEngine.sendUserMessage(requestParams); + listRef.current?.scrollList({ to: 'bottom' }); + }; + + const inputChangeHandler = (e: CustomEvent) => { + setInputValue(e.detail); + }; + + const sendHandler = async (e: CustomEvent) => { + const { value } = e.detail; + const params = { + prompt: value, + }; + await sendUserMessage(params); + setInputValue(''); + }; + + const stopHandler = () => { + chatEngine.abortChat(); + }; + + useEffect(() => { + if (!chatEngine) { + return; + } + // 此处增加自定义消息内容合并策略逻辑 + // 该示例agent类型结构比较复杂,根据任务步骤的state有不同的策略,组件内onMessage这里提供了的strategy无法满足,可以通过注册合并策略自行实现 + chatEngine.registerMergeStrategy('agent', (newChunk, existing) => { + // 创建新对象避免直接修改原状态 + const updated = { + ...existing, + content: { + ...existing.content, + steps: [...existing.content.steps], + }, + }; + const stepIndex = updated.content.steps.findIndex((step) => step.agent_id === newChunk.content.agent_id); + + if (stepIndex === -1) return updated; + + // 更新步骤信息 + const step = { + ...updated.content.steps[stepIndex], + tasks: [...(updated.content.steps[stepIndex].tasks || [])], + status: newChunk.state === 'finish' ? 'finish' : 'pending', + }; + + // 处理不同类型的新数据 + if (newChunk.state === 'command') { + // 新增每个步骤执行的命令 + step.tasks.push({ + type: 'command', + text: newChunk.content.text, + }); + } else if (newChunk.state === 'result') { + // 新增每个步骤执行的结论是流式输出,需要分情况处理 + const resultTaskIndex = step.tasks.findIndex((task) => task.type === 'result'); + if (resultTaskIndex >= 0) { + // 合并到已有结果 + step.tasks = step.tasks.map((task, index) => + index === resultTaskIndex ? { ...task, text: task.text + newChunk.content.text } : task, + ); + } else { + // 添加新结果 + step.tasks.push({ + type: 'result', + text: newChunk.content.text, + }); + } + } + + updated.content.steps[stepIndex] = step; + return updated; + }); + }, [chatEngine]); + + return ( +
+ + {messages.map((message, idx) => ( + + {renderMsgContents(message, idx === messages.length - 1)} + + ))} + + +
+ ); +} diff --git a/packages/pro-components/chat/chatbot/_example/agui.tsx b/packages/pro-components/chat/chatbot/_example/agui.tsx new file mode 100644 index 0000000000..b81f8eab8a --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/agui.tsx @@ -0,0 +1,163 @@ +import React, { ReactNode, useMemo, useRef, useState } from 'react'; +import { + type TdChatMessageConfig, + type ChatRequestParams, + type ChatMessagesData, + type ChatBaseContent, + ChatList, + ChatSender, + ChatMessage, + TdChatListApi, + TdChatSenderApi, + ChatActionBar, + isAIMessage, + useChat, +} from '@tdesign-react/aigc'; +import { getMessageContentForCopy, TdChatActionsName, TdChatSenderParams } from 'tdesign-web-components'; +import mockData from './mock/data'; + +export default function ComponentsBuild() { + const listRef = useRef(null); + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState('南极的自动提款机叫什么名字'); + const { chatEngine, messages, status } = useChat({ + defaultMessages: mockData.normal, + // 聊天服务配置 + chatServiceConfig: { + // 对话服务地址 + endpoint: `http://127.0.0.1:3000/sse/agui`, + // endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/agent`, + protocol: 'agui', + stream: true, + onStart: (chunk) => { + console.log('onStart', chunk); + }, + // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) + onComplete: (aborted: boolean, params: RequestInit, event) => { + console.log('onComplete', aborted, params, event); + }, + // 流式对话过程中出错业务自定义行为 + onError: (err: Error | Response) => { + console.error('Chatservice Error:', err); + }, + // 流式对话过程中用户主动结束对话业务自定义行为 + onAbort: async () => {}, + // 自定义请求参数 + onRequest: (innerParams: ChatRequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'agent_uid', + prompt, + }), + }; + }, + }, + }); + + const senderLoading = useMemo(() => { + if (status === 'pending' || status === 'streaming') { + return true; + } + return false; + }, [status]); + + // 消息属性配置 + const messageProps: TdChatMessageConfig = { + user: { + variant: 'base', + placement: 'right', + }, + assistant: { + placement: 'left', + // 内置的消息渲染配置 + chatContentProps: { + thinking: { + maxHeight: 100, + }, + }, + }, + }; + + const getChatActionBar = (isLast: boolean) => { + let filterActions = ['replay', 'good', 'bad', 'copy']; + if (!isLast) { + // 只有最后一条AI消息才能重新生成 + filterActions = filterActions.filter((item) => item !== 'replay'); + } + return filterActions; + }; + + const actionHandler = (name: string, data?: any) => { + switch (name) { + case 'replay': { + console.log('自定义重新回复'); + chatEngine.regenerateAIMessage(); + return; + } + default: + console.log('触发action', name, 'data', data); + } + }; + + /** 渲染消息内容体 */ + const renderMsgContents = (message: ChatMessagesData, isLast: boolean): ReactNode => ( + <> + {isAIMessage(message) && message.status === 'complete' ? ( + + ) : null} + + ); + + const sendUserMessage = async (requestParams: ChatRequestParams) => { + await chatEngine.sendUserMessage(requestParams); + listRef.current?.scrollList({ to: 'bottom' }); + }; + + const inputChangeHandler = (e: CustomEvent) => { + setInputValue(e.detail); + }; + + const sendHandler = async (e: CustomEvent) => { + const { value } = e.detail; + const params = { + prompt: value, + }; + await sendUserMessage(params); + setInputValue(''); + }; + + const stopHandler = () => { + chatEngine.abortChat(); + }; + + return ( +
+ + {messages.map((message, idx) => ( + + {renderMsgContents(message, idx === messages.length - 1)} + + ))} + + +
+ ); +} diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 7911805582..dc66921a4b 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -171,7 +171,7 @@ export default function ComponentsBuild() { }; const onScrollHandler = (e) => { - console.log('===scroll', e, e.detail); + // console.log('===scroll', e, e.detail); }; return ( diff --git a/packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md b/packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md new file mode 100644 index 0000000000..4aba113872 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md @@ -0,0 +1,261 @@ +# AG-UI 协议适配器 + +TDesign Chatbot 组件的 AG-UI(Agent User Interaction Protocol)协议适配器,支持标准化AI代理通信。 + +## 概述 + +AG-UI 是一个用于前端应用与 AI 代理通信的标准化协议。本适配器使 TDesign Chatbot 组件能够: + +1. **兼容 AG-UI 标准**:与遵循 AG-UI 协议的服务端和前端工具无缝集成 +2. **向后兼容**:保持对现有 TDesign 格式的完全支持 +3. **灵活适配**:自动转换不同数据格式,提供可插拔的适配层 + +## 三种使用场景 + +### 场景 1:服务端原生 AG-UI 协议 + +**适用于**:服务端已经支持 AG-UI 协议的新项目 + +```typescript +import { createAGUIAdapter } from './adapters/agui-adapter'; + +const aguiConfig = { + mode: 'native' as const, + agentId: 'my-agent', + onEvent: (event) => { + console.log('AG-UI事件:', event); + } +}; + +const adapter = createAGUIAdapter(aguiConfig); +const config = adapter.wrapConfig({ + endpoint: '/api/chat/agui-native', + stream: true +}); + +const engine = new ChatEngine(); +engine.init(config); +``` + +**服务端响应格式**: +```json +{ + "type": "TEXT_MESSAGE_CHUNK", + "data": { + "content": "你好!", + "contentType": "text" + }, + "timestamp": 1234567890, + "runId": "run_abc", + "agentId": "my-agent" +} +``` + +### 场景 2:适配器转换模式 + +**适用于**:现有项目需要 AG-UI 标准化,但服务端暂未支持 + +```typescript +const aguiConfig = { + mode: 'adapter' as const, + agentId: 'my-agent', + onEvent: (event) => { + // 接收转换后的标准AG-UI事件 + console.log('转换后的AG-UI事件:', event); + }, + // 可选:自定义适配逻辑 + customAdapter: (chunk) => { + if (chunk.data?.type === 'special_format') { + return { + type: 'CUSTOM', + data: { type: 'special', content: chunk.data.content } + }; + } + return null; // 使用默认适配器 + } +}; + +const adapter = createAGUIAdapter(aguiConfig); +const config = adapter.wrapConfig({ + endpoint: '/api/chat/tdesign-format', + stream: true +}); +``` + +**服务端响应格式**(现有TDesign格式): +```json +{"type": "text", "msg": "你好!"} +{"type": "thinking", "content": "正在思考...", "title": "思考中"} +``` + +**自动转换为**: +```json +{ + "type": "TEXT_MESSAGE_CHUNK", + "data": {"content": "你好!", "contentType": "text"} +} +{ + "type": "CUSTOM", + "data": {"type": "thinking", "content": "正在思考...", "title": "思考中"} +} +``` + +### 场景 3:传统回调模式 + +**适用于**:现有项目无需 AG-UI 标准化 + +```typescript +const config = { + endpoint: '/api/chat/traditional', + stream: true, + callbacks: { + onMessage: (chunk) => { + // 自定义业务逻辑 + if (chunk.data?.type === 'text') { + return { + type: 'text', + data: chunk.data.msg, + strategy: 'append' + }; + } + return null; + }, + onComplete: (isAborted, params, result) => { + console.log('对话完成'); + } + } +}; + +// 直接使用,无需适配器 +const engine = new ChatEngine(); +engine.init(config); +``` + +## API 参考 + +### AGUIConfig + +```typescript +interface AGUIConfig { + /** 使用模式 */ + mode: 'disabled' | 'native' | 'adapter'; + + /** Agent ID */ + agentId?: string; + + /** 是否启用双向通信 */ + bidirectional?: boolean; + + /** AG-UI事件处理器 */ + onEvent?: (event: AGUIEvent) => void; + + /** 自定义内容解析器 */ + contentParser?: (event: AGUIEvent) => AIContentChunkUpdate | null; + + /** 自定义适配器(adapter模式) */ + customAdapter?: (chunk: SSEChunkData) => AGUIEvent | null; +} +``` + +### AG-UI 标准事件类型 + +- **生命周期事件**:`RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` +- **文本消息事件**:`TEXT_MESSAGE_START`, `TEXT_MESSAGE_CHUNK`, `TEXT_MESSAGE_END` +- **工具调用事件**:`TOOL_CALL_START`, `TOOL_CALL_CHUNK`, `TOOL_CALL_END` +- **状态管理事件**:`STATE_SNAPSHOT`, `STATE_DELTA`, `MESSAGES_SNAPSHOT` +- **扩展事件**:`RAW`, `CUSTOM` + +### 工具函数 + +```typescript +// 创建适配器 +const adapter = createAGUIAdapter(config); + +// 事件类型检查 +import { AGUIUtils } from './adapters/agui-adapter'; + +AGUIUtils.isTextEvent(event); // 检查是否为文本事件 +AGUIUtils.isToolEvent(event); // 检查是否为工具事件 +AGUIUtils.isLifecycleEvent(event); // 检查是否为生命周期事件 +AGUIUtils.isStateEvent(event); // 检查是否为状态事件 +``` + +## 默认适配规则 + +### TDesign → AG-UI 转换 + +| TDesign 格式 | AG-UI 事件类型 | 说明 | +|-------------|---------------|------| +| `{type: "text", msg: "..."}` | `TEXT_MESSAGE_CHUNK` | 文本消息 | +| `{type: "markdown", msg: "..."}` | `TEXT_MESSAGE_CHUNK` | Markdown消息 | +| `{type: "thinking", content: "..."}` | `CUSTOM` | 思考过程 | +| `{type: "search", query: "..."}` | `CUSTOM` | 搜索动作 | +| 字符串 | `TEXT_MESSAGE_CHUNK` | 纯文本 | + +### AG-UI → AIContentChunkUpdate 转换 + +| AG-UI 事件 | TDesign 内容类型 | 策略 | +|-----------|----------------|------| +| `TEXT_MESSAGE_CHUNK` | `text` / `markdown` | `append` | +| `TOOL_CALL_*` | `search` | `append` | +| `CUSTOM` (thinking) | `thinking` | `append` | +| `RUN_ERROR` | `text` | `append` | + +## 最佳实践 + +### 1. 选择合适的模式 + +- **新项目 + AG-UI服务端** → `native` 模式 +- **现有项目 + 需要标准化** → `adapter` 模式 +- **现有项目 + 无需AG-UI** → 传统回调模式 + +### 2. 事件处理 + +```typescript +// ✅ 好的做法:分离关注点 +onEvent: (event) => { + // AG-UI协议层:发送到外部系统 + websocket.send(JSON.stringify(event)); + analytics.track('agui_event', event); + + // 不要在这里处理UI业务逻辑 +} + +// ✅ 业务逻辑在内容解析器中处理 +contentParser: (event) => { + if (event.type === 'TEXT_MESSAGE_CHUNK') { + updateChatUI(event.data.content); + return { + type: 'text', + data: event.data.content, + strategy: 'append' + }; + } + return null; +} +``` + +### 3. 自定义适配器 + +```typescript +customAdapter: (chunk) => { + // 只处理特殊格式,其他交给默认适配器 + if (chunk.data?.type === 'special_format') { + return { + type: 'CUSTOM', + data: transformSpecialFormat(chunk.data) + }; + } + return null; // 使用默认适配器 +} +``` + +## 示例代码 + +完整的使用示例请参考:`src/chatbot/_example/agui-scenarios-example.tsx` + +## 相关资源 + +- [AG-UI 官方文档](https://docs.ag-ui.com/) +- [AG-UI GitHub](https://github.com/ag-ui-protocol/ag-ui) +- [TDesign Chatbot 文档](./README.md) \ No newline at end of file diff --git a/packages/pro-components/chat/chatbot/core/adapters/README.md b/packages/pro-components/chat/chatbot/core/adapters/README.md new file mode 100644 index 0000000000..3624eeceda --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/README.md @@ -0,0 +1,454 @@ +# TDesign Chatbot AG-UI 协议适配器 + +为TDesign Web Components的Chatbot组件提供AG-UI协议支持,支持与标准化AI代理通信协议的无缝集成。 + +## 🎯 设计目标 + +- **配置分离**:网络配置、业务回调、协议转换完全独立 +- **互斥模式**:传统回调与AG-UI事件处理二选一,避免混淆 +- **向后兼容**:不启用时零影响,启用时可选择兼容模式 +- **职责清晰**:业务逻辑、协议通信、技术监控分层处理 + +## 📋 三种配置模式 + +### 1. 传统回调模式 + +使用原有的`callbacks`配置,适合现有项目迁移: + +```typescript +const config: ChatServiceConfig = { + // 网络配置 + endpoint: 'http://localhost:3000/sse/chat', + stream: true, + retryInterval: 1000, + maxRetries: 3, + + // 传统业务回调 + callbacks: { + onRequest: (params) => { + console.log('发送请求:', params); + return { headers: { 'Content-Type': 'application/json' } }; + }, + + onMessage: (chunk, message) => { + console.log('收到消息:', chunk); + // 解析并返回内容 + return { type: 'text', data: chunk.data }; + }, + + onComplete: (isAborted) => { + console.log('对话完成:', isAborted); + }, + + onError: (error) => { + console.error('处理错误:', error); + }, + }, + + // 连接技术监控 + connection: { + onHeartbeat: (event) => console.log('连接心跳'), + onConnectionStateChange: (event) => console.log('连接状态变化'), + }, +}; +``` + +**特点:** +- ✅ 使用熟悉的回调API +- ✅ 适合现有项目无缝迁移 +- ❌ 无AG-UI协议功能 + +### 2. AG-UI纯模式(推荐新项目) + +完全基于AG-UI事件驱动,不使用传统回调: + +```typescript +const config: ChatServiceConfig = { + // 网络配置 + endpoint: 'http://localhost:3000/sse/chat', + stream: true, + + // ⚠️ 注意:AG-UI纯模式下不配置callbacks! + // callbacks: undefined, + + // AG-UI协议配置 + agui: { + enabled: true, + agentId: 'my-chatbot', + bidirectional: true, + + // 业务逻辑处理(替代传统callbacks) + onBusinessEvent: (event: AGUIEvent) => { + console.log('AG-UI业务事件:', event); + + switch (event.type) { + case 'RUN_STARTED': + console.log('🚀 对话开始'); + break; + + case 'TEXT_MESSAGE_CHUNK': + console.log('📝 接收文本:', event.data.content); + // 在这里处理UI更新逻辑 + updateChatUI(event.data.content); + break; + + case 'TOOL_CALL_CHUNK': + console.log('🔧 工具调用:', event.data.toolName); + break; + + case 'RUN_FINISHED': + console.log('✅ 对话完成:', event.data.reason); + enableInputField(); + break; + + case 'RUN_ERROR': + console.error('❌ 运行错误:', event.data.error); + showErrorMessage(event.data.error); + break; + } + }, + + // 协议通信(发送到外部系统) + onProtocolEvent: (event: AGUIEvent) => { + console.log('📡 协议事件:', event.type); + + // 发送到外部AG-UI兼容系统 + websocket.send(JSON.stringify(event)); + analytics.track('agui_event', event); + messageQueue.publish('agui-events', event); + }, + + // 外部事件处理(双向通信) + onExternalEvent: (event: AGUIEvent) => { + console.log('🔄 外部事件:', event); + // 处理外部系统发送的AG-UI事件 + }, + }, +}; +``` + +**特点:** +- ✅ 完全基于AG-UI标准事件 +- ✅ 支持双向通信 +- ✅ 现代化事件驱动架构 +- ✅ 与外部AG-UI系统无缝集成 +- ❌ 需要学习AG-UI事件API + +### 3. 传统兼容模式 + +同时支持传统回调和AG-UI协议,适合渐进迁移: + +```typescript +const config: ChatServiceConfig = { + // 网络配置 + endpoint: 'http://localhost:3000/sse/chat', + stream: true, + + // 传统业务回调(保持原有逻辑不变) + callbacks: { + onMessage: (chunk, message) => { + console.log('💬 传统业务处理:', chunk); + // 原有的业务逻辑保持不变 + return { type: 'text', data: String(chunk.data) }; + }, + + onComplete: (isAborted) => { + console.log('🏁 传统完成处理:', isAborted); + enableInputField(); + }, + + onError: (error) => { + console.error('🚨 传统错误处理:', error); + showErrorMessage(error); + }, + }, + + // 同时启用AG-UI协议转换 + agui: { + enabled: true, + agentId: 'compatibility-bot', + + // 仅用于协议通信,不处理业务逻辑 + onProtocolEvent: (event: AGUIEvent) => { + console.log('📡 AG-UI协议事件:', event.type); + + // 发送到外部AG-UI兼容系统 + websocket.send(JSON.stringify(event)); + fetch('/api/agui-events', { + method: 'POST', + body: JSON.stringify(event) + }); + }, + }, +}; +``` + +**特点:** +- ✅ 保持原有业务逻辑不变 +- ✅ 增加AG-UI协议支持 +- ✅ 业务逻辑与协议通信分离 +- ✅ 适合现有项目渐进迁移 +- ⚠️ 两套API同时存在 + +## 🔧 AG-UI事件类型 + +AG-UI协议定义了16种标准事件类型: + +| 事件类型 | 描述 | 数据结构 | +|---------|------|----------| +| `RUN_STARTED` | 对话开始 | `{ prompt, messageId, attachments }` | +| `TEXT_MESSAGE_CHUNK` | 文本消息块 | `{ content, messageId, contentType? }` | +| `TOOL_CALL_CHUNK` | 工具调用块 | `{ toolName, action, input }` | +| `TOOL_RESULT_CHUNK` | 工具结果块 | `{ toolName, result, success }` | +| `INPUT_REQUEST` | 请求用户输入 | `{ requestId, prompt, options }` | +| `RUN_FINISHED` | 对话结束 | `{ success, reason, result? }` | +| `RUN_ERROR` | 运行错误 | `{ error, details }` | +| `HEARTBEAT` | 心跳检测 | `{ connectionId, timestamp }` | +| `STATE_CHANGE` | 状态变化 | `{ from, to, connectionId }` | +| `CONNECTION_ESTABLISHED` | 连接建立 | `{ connectionId }` | +| `CONNECTION_LOST` | 连接断开 | `{ connectionId, reason }` | +| `USER_INPUT` | 用户输入 | `{ requestId, input }` | +| `AGENT_MESSAGE` | 代理消息 | `{ type, content, title? }` | +| `SYSTEM_MESSAGE` | 系统消息 | `{ content, level }` | +| `METADATA_UPDATE` | 元数据更新 | `{ type, data }` | + +## 🎨 使用示例 + +### 基础使用 + +```tsx +import { Component } from 'omi'; +import type { ChatServiceConfig } from 'tdesign-web-components/chatbot'; + +export default class MyChatBot extends Component { + // AG-UI纯模式配置 + chatConfig: ChatServiceConfig = { + endpoint: '/api/chat', + stream: true, + + agui: { + enabled: true, + agentId: 'my-assistant', + bidirectional: true, + + onBusinessEvent: (event) => { + // 处理所有业务逻辑 + this.handleBusinessEvent(event); + }, + + onProtocolEvent: (event) => { + // 发送到外部系统 + this.sendToExternalSystem(event); + }, + }, + }; + + handleBusinessEvent(event) { + switch (event.type) { + case 'TEXT_MESSAGE_CHUNK': + // 更新UI显示 + this.updateChatDisplay(event.data.content); + break; + case 'RUN_FINISHED': + // 启用输入框 + this.enableInput(); + break; + } + } + + render() { + return ( + console.log('聊天就绪')} + /> + ); + } +} +``` + +### 双向通信 + +```typescript +// 请求用户输入 +const userInput = await chatEngine.requestUserInput( + '请选择你的偏好设置:', + { type: 'select', options: ['A', 'B', 'C'] } +); + +// 处理外部AG-UI事件 +chatEngine.handleAGUIEvent({ + type: 'USER_INPUT', + data: { requestId: 'req_123', input: 'A' }, + timestamp: Date.now(), +}); +``` + +### 自定义事件映射 + +```typescript +const config: ChatServiceConfig = { + agui: { + enabled: true, + + // 自定义事件映射 + eventMapping: { + 'TEXT_MESSAGE_CHUNK': 'custom_text', + 'RUN_STARTED': 'session_begin', + 'RUN_FINISHED': 'session_end', + }, + + onProtocolEvent: (event) => { + // 事件类型已经被映射 + console.log('映射后的事件:', event.type); + }, + }, +}; +``` + +## 📚 配置对比表 + +| 配置项 | 传统模式 | AG-UI纯模式 | 兼容模式 | +|-------|---------|------------|----------| +| `callbacks` | ✅ 必需 | ❌ 不使用 | ✅ 保留 | +| `agui.enabled` | ❌ 不启用 | ✅ 必需 | ✅ 启用 | +| `agui.onBusinessEvent` | ❌ 不使用 | ✅ 必需 | ❌ 不使用 | +| `agui.onProtocolEvent` | ❌ 不使用 | ✅ 可选 | ✅ 推荐 | +| 业务逻辑处理 | callbacks | onBusinessEvent | callbacks | +| AG-UI协议支持 | 无 | 完整 | 仅协议转换 | +| 迁移难度 | 无需迁移 | 需要重写 | 无需更改 | +| 推荐场景 | 现有项目 | 新项目 | 渐进迁移 | + +## 🔍 调试和监控 + +### 获取适配器状态 + +```typescript +const adapter = chatEngine.getAGUIAdapter(); +if (adapter) { + console.log('适配器状态:', adapter.getState()); +} +``` + +### 监听协议事件 + +```typescript +// 监听所有AG-UI协议事件 +window.addEventListener('agui-protocol-event', (event) => { + console.log('收到AG-UI事件:', event.detail); +}); +``` + +### 调试日志 + +启用AG-UI适配器后,控制台会显示详细的运行模式信息: + +``` +🤖 [TDesign-Chatbot] AG-UI协议适配器已启用 - AG-UI纯模式 +{ + agentId: "my-chatbot", + bidirectional: true, + mode: "AG-UI纯模式", + hasCallbacks: false, + hasBusinessEvent: true +} +``` + +## 🚀 最佳实践 + +### 1. 选择合适的模式 + +- **新项目**:使用AG-UI纯模式,获得最佳的事件驱动体验 +- **现有项目**:使用传统兼容模式,渐进式增加AG-UI支持 +- **简单项目**:使用传统模式,保持简单 + +### 2. 事件处理分离 + +```typescript +// ✅ 正确:职责分离 +const config = { + agui: { + enabled: true, + + // 业务逻辑:处理UI更新、状态管理 + onBusinessEvent: (event) => { + updateUI(event); + updateState(event); + }, + + // 协议通信:发送到外部系统 + onProtocolEvent: (event) => { + websocket.send(JSON.stringify(event)); + analytics.track('agui_event', event); + }, + }, +}; + +// ❌ 错误:职责混淆 +const config = { + agui: { + onProtocolEvent: (event) => { + // 不要在协议层处理业务逻辑 + updateUI(event); // 错误! + websocket.send(JSON.stringify(event)); // 正确 + }, + }, +}; +``` + +### 3. 错误处理 + +```typescript +const config = { + agui: { + enabled: true, + + onBusinessEvent: (event) => { + try { + handleBusinessLogic(event); + } catch (error) { + console.error('业务逻辑错误:', error); + // 不要让业务错误影响协议通信 + } + }, + + onProtocolEvent: (event) => { + try { + sendToExternalSystem(event); + } catch (error) { + console.error('协议通信错误:', error); + // 协议错误不应影响主业务流程 + } + }, + }, +}; +``` + +## 🔗 相关链接 + +- [AG-UI协议官方文档](https://docs.ag-ui.com) +- [TDesign Chatbot组件文档](../README.md) +- [示例代码](../_example/agui-clear-example.tsx) + +## 📋 FAQ + +### Q: 如何从传统模式迁移到AG-UI模式? + +A: 推荐使用传统兼容模式作为过渡: + +1. 启用AG-UI适配器但保留原有callbacks +2. 逐步将业务逻辑迁移到onBusinessEvent +3. 最后删除callbacks配置 + +### Q: 可以同时使用callbacks和onBusinessEvent吗? + +A: 不建议。两者是互斥的: +- 有callbacks:传统兼容模式,onBusinessEvent不生效 +- 无callbacks:AG-UI纯模式,使用onBusinessEvent + +### Q: AG-UI协议事件与传统回调有什么区别? + +A: 主要区别: +- **传统回调**:函数式API,直接处理SSE数据 +- **AG-UI事件**:标准化事件格式,包含runId、agentId等元数据 +- **适用场景**:AG-UI适合多代理通信,传统回调适合简单场景 \ No newline at end of file diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts b/packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts new file mode 100644 index 0000000000..a62c21856f --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts @@ -0,0 +1,561 @@ +/** + * AG-UI Protocol Adapter + * + * AG-UI协议适配器 - 支持三种使用场景: + * 1. 服务端完全按照AG-UI协议返回(直接解析AG-UI事件) + * 2. 服务端未按照AG-UI协议,业务提供适配器转换 + * 3. 服务端未按照AG-UI协议,保持传统回调模式 + */ + +import type { + AIContentChunkUpdate, + ChatMessagesData, + ChatRequestParams, + ChatServiceConfig, + SSEChunkData, +} from '../type'; + +// AG-UI适配器配置(前向声明) +export interface AGUIAdapterConfig { + /** 是否启用AG-UI协议适配 */ + enabled: boolean; + /** Agent ID,用于标识当前AI代理 */ + agentId?: string; + /** 是否启用双向通信(支持INPUT_REQUEST) */ + bidirectional?: boolean; + /** 自定义事件映射 */ + eventMapping?: Partial>; + /** AG-UI事件处理器 */ + onProtocolEvent?: (event: any) => void; + /** 发送AG-UI事件的处理器 */ + onExternalEvent?: (event: any) => void; + /** AG-UI业务事件处理器 - AG-UI纯模式下替代传统callbacks */ + onBusinessEvent?: (event: any) => void; +} + +// ============================================================================= +// AG-UI 标准协议事件类型(严格遵循官方协议) +// ============================================================================= + +/** AG-UI协议标准事件类型 */ +export type AGUIEventType = + // 生命周期事件 + | 'RUN_STARTED' // 对话开始 + | 'RUN_FINISHED' // 对话完成 + | 'RUN_ERROR' // 对话出错 + | 'STEP_STARTED' // 步骤开始 + | 'STEP_FINISHED' // 步骤完成 + + // 文本消息事件 + | 'TEXT_MESSAGE_START' // 文本消息开始 + | 'TEXT_MESSAGE_CHUNK' // 文本消息块(流式) + | 'TEXT_MESSAGE_END' // 文本消息结束 + + // 工具调用事件 + | 'TOOL_CALL_START' // 工具调用开始 + | 'TOOL_CALL_CHUNK' // 工具调用块 + | 'TOOL_CALL_END' // 工具调用结束 + + // 状态管理事件 + | 'STATE_SNAPSHOT' // 状态快照 + | 'STATE_DELTA' // 状态增量更新 + | 'MESSAGES_SNAPSHOT' // 消息快照 + + // 扩展事件 + | 'RAW' // 原始事件 + | 'CUSTOM'; // 自定义事件 + +// AG-UI 标准事件数据结构 +export interface AGUIEvent { + type: AGUIEventType; + data: T; + timestamp?: number; + runId?: string; + agentId?: string; + messageId?: string; + threadId?: string; + metadata?: Record; +} + +// ============================================================================= +// AG-UI 使用配置 +// ============================================================================= + +/** AG-UI使用模式 */ +export type AGUIMode = + | 'disabled' // 禁用AG-UI,使用传统回调 + | 'native' // 服务端原生AG-UI协议 + | 'adapter'; // 服务端非AG-UI,需要业务提供适配器 + +/** AG-UI配置 */ +export interface AGUIConfig { + /** 使用模式 */ + mode: AGUIMode; + + /** Agent ID */ + agentId?: string; + + /** 是否启用双向通信 */ + bidirectional?: boolean; + + /** + * AG-UI事件处理器 + * - native模式:处理服务端直接返回的AG-UI事件 + * - adapter模式:处理适配器转换后的AG-UI事件 + */ + onEvent?: (event: AGUIEvent) => void; + + /** + * 自定义内容解析器 + * 如果不提供,使用默认解析器将AG-UI事件转换为AIContentChunkUpdate + */ + contentParser?: (event: AGUIEvent) => AIContentChunkUpdate | null; + + /** + * 自定义适配器(adapter模式下必需) + * 将业务自定义的chunk格式转换为AG-UI标准格式 + * + * 注意:TDesign Chatbot本身没有标准的chunk格式, + * 每个业务的chunk.data结构都不同,因此必须提供此函数 + */ + customAdapter?: (chunk: SSEChunkData) => AGUIEvent | null; +} + +// ============================================================================= +// 默认内容解析器:AG-UI事件 → AIContentChunkUpdate +// ============================================================================= + +/** + * 默认AG-UI事件解析器 + * 将AG-UI标准事件转换为组件渲染需要的AIContentChunkUpdate结构 + */ +export function parseAGUIEventToContent(event: AGUIEvent): AIContentChunkUpdate | null { + switch (event.type) { + case 'TEXT_MESSAGE_CHUNK': + return { + type: event.data.contentType === 'markdown' ? 'markdown' : 'text', + data: event.data.content || event.data.text || '', + strategy: 'append' as const, + }; + + case 'TEXT_MESSAGE_START': + return { + type: 'text', + data: '', + strategy: 'append' as const, + }; + + case 'TEXT_MESSAGE_END': + return { + type: 'text', + data: event.data.finalContent || '', + strategy: 'append' as const, + }; + + case 'TOOL_CALL_START': + case 'TOOL_CALL_CHUNK': + case 'TOOL_CALL_END': + return { + type: 'search', + data: { + title: event.data.toolName || 'Tool Call', + references: [], + }, + strategy: 'append' as const, + }; + + case 'RUN_ERROR': + return { + type: 'text', + data: event.data.error || event.data.message || 'Unknown error', + strategy: 'append' as const, + }; + + case 'CUSTOM': + // 处理自定义事件,尝试解析为通用格式 + if (event.data.type === 'thinking') { + return { + type: 'thinking', + data: { + text: event.data.content || event.data.text || '', + title: event.data.title, + }, + strategy: 'append' as const, + }; + } + + if (event.data.type === 'search') { + return { + type: 'search', + data: { + title: 'Search', + references: [], + }, + strategy: 'append' as const, + }; + } + + return { + type: 'text', + data: event.data.content || event.data.text || JSON.stringify(event.data), + strategy: 'append' as const, + }; + + default: + // 忽略生命周期事件(RUN_STARTED, RUN_FINISHED等) + return null; + } +} + +// ============================================================================= +// AG-UI 适配器类 +// ============================================================================= + +export class AGUIAdapter { + private config: AGUIConfig; + + private currentRunId: string | null = null; + + constructor(config: AGUIConfig) { + this.config = { + agentId: 'tdesign-chatbot', + bidirectional: false, + ...config, + }; + } + + /** + * 包装ChatServiceConfig以支持AG-UI + */ + public wrapConfig(originalConfig: ChatServiceConfig): ChatServiceConfig { + if (this.config.mode === 'disabled') { + return originalConfig; + } + + // adapter模式下检查必需的customAdapter + if (this.config.mode === 'adapter' && !this.config.customAdapter) { + throw new Error( + '[AGUIAdapter] adapter模式下必须提供customAdapter函数,' + + '因为TDesign Chatbot本身没有标准的chunk格式,' + + '每个业务的chunk.data结构都不同', + ); + } + + return { + ...originalConfig, + callbacks: this.createAGUICallbacks(originalConfig), + }; + } + + /** + * 创建AG-UI模式的回调配置 + */ + private createAGUICallbacks(originalConfig: ChatServiceConfig) { + return { + onRequest: (params: ChatRequestParams) => { + this.currentRunId = this.generateRunId(); + + // 发送RUN_STARTED事件 + this.emitEvent({ + type: 'RUN_STARTED', + data: { + prompt: params.prompt, + messageId: params.messageID, + attachments: params.attachments, + }, + runId: this.currentRunId, + agentId: this.config.agentId, + }); + + return originalConfig.callbacks?.onRequest?.(params) || {}; + }, + + onMessage: (chunk: SSEChunkData, message?: ChatMessagesData) => { + const aguiEvent = this.processChunk(chunk); + + if (aguiEvent) { + // 发送AG-UI事件 + this.emitEvent(aguiEvent); + + // 解析为内容并返回给组件 + const content = this.parseEventToContent(aguiEvent); + if (content) { + return content; + } + } + + // 如果AG-UI解析失败,回退到原有逻辑 + return originalConfig.callbacks?.onMessage?.(chunk, message); + }, + + onComplete: (isAborted: boolean, params: RequestInit, result?: any) => { + // 发送RUN_FINISHED事件 + this.emitEvent({ + type: 'RUN_FINISHED', + data: { + success: !isAborted, + reason: isAborted ? 'user_aborted' : 'completed', + result, + }, + runId: this.currentRunId, + agentId: this.config.agentId, + }); + + this.currentRunId = null; + return originalConfig.callbacks?.onComplete?.(isAborted, params, result); + }, + + onError: (err: Error | Response) => { + // 发送RUN_ERROR事件 + this.emitEvent({ + type: 'RUN_ERROR', + data: { + error: err instanceof Error ? err.message : 'Request failed', + details: err, + }, + runId: this.currentRunId, + agentId: this.config.agentId, + }); + + this.currentRunId = null; + return originalConfig.callbacks?.onError?.(err); + }, + + onAbort: async () => { + this.emitEvent({ + type: 'RUN_FINISHED', + data: { + success: false, + reason: 'user_aborted', + }, + runId: this.currentRunId, + agentId: this.config.agentId, + }); + + this.currentRunId = null; + return originalConfig.callbacks?.onAbort?.(); + }, + }; + } + + /** + * 处理数据块,根据模式选择不同的处理方式 + */ + private processChunk(chunk: SSEChunkData): AGUIEvent | null { + switch (this.config.mode) { + case 'native': + // 场景1:服务端直接返回AG-UI格式 + return this.parseNativeAGUIChunk(chunk); + + case 'adapter': + // 场景2:需要业务提供适配转换 + return this.adaptChunkToAGUI(chunk); + + default: + return null; + } + } + + /** + * 解析原生AG-UI格式数据块 + * 服务端直接返回AG-UI标准格式:{type: 'TEXT_MESSAGE_CHUNK', data: {...}} + */ + private parseNativeAGUIChunk(chunk: SSEChunkData): AGUIEvent | null { + try { + const chunkData = chunk.data; + + // 检查是否为AG-UI标准格式 + if (chunkData && typeof chunkData === 'object' && 'type' in chunkData) { + return { + type: chunkData.type as AGUIEventType, + data: chunkData.data || chunkData, + timestamp: chunkData.timestamp || Date.now(), + runId: chunkData.runId || this.currentRunId, + agentId: chunkData.agentId || this.config.agentId, + messageId: chunkData.messageId, + threadId: chunkData.threadId, + metadata: chunkData.metadata, + }; + } + + // 如果不是标准格式,当作TEXT_MESSAGE_CHUNK处理 + if (typeof chunkData === 'string') { + return { + type: 'TEXT_MESSAGE_CHUNK', + data: { content: chunkData }, + timestamp: Date.now(), + runId: this.currentRunId, + agentId: this.config.agentId, + }; + } + } catch (error) { + console.warn('[AGUIAdapter] 解析原生AG-UI数据失败:', error); + } + + return null; + } + + /** + * 适配业务自定义格式到AG-UI格式 + * 业务必须提供customAdapter,因为TDesign没有标准chunk格式 + */ + private adaptChunkToAGUI(chunk: SSEChunkData): AGUIEvent | null { + try { + // 必须使用业务提供的自定义适配器 + if (this.config.customAdapter) { + return this.config.customAdapter(chunk); + } + + // 如果没有提供适配器,返回null(应该在wrapConfig时就检查了) + console.warn('[AGUIAdapter] adapter模式下必须提供customAdapter'); + return null; + } catch (error) { + console.warn('[AGUIAdapter] 适配数据格式失败:', error); + return null; + } + } + + /** + * 将AG-UI事件解析为内容 + */ + private parseEventToContent(event: AGUIEvent): AIContentChunkUpdate | null { + try { + // 使用自定义解析器 + if (this.config.contentParser) { + return this.config.contentParser(event); + } + + // 使用默认解析器 + return parseAGUIEventToContent(event); + } catch (error) { + console.warn('[AGUIAdapter] 解析事件内容失败:', error); + return null; + } + } + + /** + * 发送AG-UI事件 + */ + private emitEvent(event: AGUIEvent): void { + try { + // 确保事件格式完整 + const completeEvent: AGUIEvent = { + timestamp: Date.now(), + runId: this.currentRunId, + agentId: this.config.agentId, + ...event, + }; + + // 调用事件处理器 + this.config.onEvent?.(completeEvent); + } catch (error) { + console.error('[AGUIAdapter] 发送AG-UI事件失败:', error); + } + } + + /** + * 生成运行ID + */ + private generateRunId(): string { + return `run_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * 更新配置 + */ + public updateConfig(newConfig: Partial): void { + this.config = { ...this.config, ...newConfig }; + } + + /** + * 获取当前运行ID + */ + public getCurrentRunId(): string | null { + return this.currentRunId; + } +} + +// ============================================================================= +// 工厂函数和工具 +// ============================================================================= + +/** + * 创建AG-UI适配器 + */ +export function createAGUIAdapter(config: AGUIConfig): AGUIAdapter { + return new AGUIAdapter(config); +} + +/** + * AG-UI事件类型检查工具 + */ +export const AGUIUtils = { + isTextEvent: (event: AGUIEvent): boolean => + ['TEXT_MESSAGE_START', 'TEXT_MESSAGE_CHUNK', 'TEXT_MESSAGE_END'].includes(event.type), + + isToolEvent: (event: AGUIEvent): boolean => + ['TOOL_CALL_START', 'TOOL_CALL_CHUNK', 'TOOL_CALL_END'].includes(event.type), + + isLifecycleEvent: (event: AGUIEvent): boolean => + ['RUN_STARTED', 'RUN_FINISHED', 'RUN_ERROR', 'STEP_STARTED', 'STEP_FINISHED'].includes(event.type), + + isStateEvent: (event: AGUIEvent): boolean => + ['STATE_SNAPSHOT', 'STATE_DELTA', 'MESSAGES_SNAPSHOT'].includes(event.type), +}; + +// ============================================================================= +// 业务适配器示例(仅供参考) +// ============================================================================= + +/** + * 示例:某个具体业务的适配器 + * 这只是个示例,实际使用时业务需要根据自己的chunk格式编写 + */ +export function createExampleBusinessAdapter() { + return (chunk: SSEChunkData): AGUIEvent | null => { + const chunkData = chunk.data; + + // 示例业务格式1:纯文本 + if (typeof chunkData === 'string') { + return { + type: 'TEXT_MESSAGE_CHUNK', + data: { + content: chunkData, + contentType: 'text', + }, + timestamp: Date.now(), + }; + } + + // 示例业务格式2:结构化数据 + if (chunkData && typeof chunkData === 'object') { + // 假设业务定义了这样的格式 + if (chunkData.msgType === 'text') { + return { + type: 'TEXT_MESSAGE_CHUNK', + data: { + content: chunkData.content, + contentType: 'text', + }, + timestamp: Date.now(), + }; + } + + if (chunkData.msgType === 'thinking') { + return { + type: 'CUSTOM', + data: { + type: 'thinking', + content: chunkData.thought, + title: chunkData.thinkingTitle, + }, + timestamp: Date.now(), + }; + } + + // 处理其他业务自定义格式... + } + + return null; + }; +} diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts new file mode 100644 index 0000000000..2a888df705 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts @@ -0,0 +1,159 @@ +/* eslint-disable class-methods-use-this */ +import type { AIMessageContent, SSEChunkData } from '../../type'; +import { EventType } from './events'; + +/** + * AGUIEventMapper + * 将AG-UI协议事件(SSEChunkData)转换为AIMessageContent[] + * 支持多轮对话、增量文本、工具调用、思考、状态快照、消息快照等基础事件 + */ +export class AGUIEventMapper { + private currentMessageId: string | null = null; + + private currentContent: AIMessageContent[] = []; + + private toolCallMap: Record = {}; + + /** + * 主入口:将SSE事件转换为AIMessageContent[] + */ + mapEvent(chunk: SSEChunkData): AIMessageContent | AIMessageContent[] | null { + const event = chunk.data; + if (!event?.type) return null; + switch (event.type) { + case 'TEXT_MESSAGE_START': + return { + type: 'markdown', + status: 'streaming', + data: '', + strategy: 'append', + }; + case 'TEXT_MESSAGE_CHUNK': + case 'TEXT_MESSAGE_END': + return { + type: 'markdown', + status: event.type === 'TEXT_MESSAGE_END' ? 'complete' : 'streaming', + data: event.delta || '', + strategy: 'merge', + }; + case EventType.THINKING_START: + return { + type: 'thinking', + data: { title: event.title || '思考中...' }, + status: 'streaming', + strategy: 'append', + }; + case EventType.THINKING_TEXT_MESSAGE_CONTENT: + return { type: 'thinking', data: { text: event.delta }, status: 'streaming', strategy: 'merge' }; + case EventType.THINKING_END: + console.log('=====think end', event); + return { type: 'thinking', data: { title: event.title || '思考结束' }, status: 'complete' }; + + case EventType.TOOL_CALL_START: + case EventType.TOOL_CALL_ARGS: + this.toolCallMap[event.toolCallId] = { + name: event.toolCallName, + arguments: event.type === 'TOOL_CALL_ARGS' ? event.delta : '', + }; + if (event.toolCallName === 'search') { + return { + type: 'search', + data: { + title: '联网搜索中', + references: [], + }, + status: 'pending', + }; + } + return null; + case EventType.TOOL_CALL_CHUNK: + case EventType.TOOL_CALL_RESULT: + console.log('====parsed', event); + if (event.toolCallName === 'search') { + let parsed = { + title: '搜索中', + references: [], + }; + try { + parsed = JSON.parse(event?.delta || event?.content); + } catch {} + return { + type: 'search', + data: parsed, + status: event.type === 'TOOL_CALL_RESULT' ? 'complete' : 'streaming', + }; + } + return null; + case EventType.STATE_SNAPSHOT: + return this.handleStateSnapshot(event.snapshot); + case EventType.MESSAGES_SNAPSHOT: + return this.handleMessagesSnapshot(event.messages); + case EventType.CUSTOM: + return this.handleCustomEvent(event); + case EventType.RUN_ERROR: + return [ + { + type: 'text', + data: event.message || event.error || 'Unknown error', + status: 'error', + }, + ]; + default: + return null; + } + } + + private handleStateSnapshot(snapshot: any): AIMessageContent[] { + // 只取assistant消息 + if (!snapshot?.messages) return []; + return snapshot.messages.flatMap((msg: any) => { + if (msg.role === 'assistant' && Array.isArray(msg.content)) { + return msg.content.map((content: any) => ({ + type: content.type || 'markdown', + data: content.data, + status: 'complete', + })); + } + return []; + }); + } + + private handleMessagesSnapshot(messages: any[]): AIMessageContent[] { + // 只取assistant消息 + if (!messages) return []; + return messages.flatMap((msg: any) => { + if (msg.role === 'assistant' && Array.isArray(msg.content)) { + return msg.content.map((content: any) => ({ + type: content.type || 'markdown', + data: content.data, + status: 'complete', + })); + } + return []; + }); + } + + private handleCustomEvent(event: any): AIMessageContent { + if (event.name === 'suggestion') { + return { + type: 'suggestion', + data: event.value?.suggestions || [], + status: 'complete', + }; + } + // 兜底:以text类型输出 + return { + type: 'text', + data: event.value?.content || event.value?.text || JSON.stringify(event.value), + status: 'complete', + }; + } + + reset() { + this.currentMessageId = null; + this.currentContent = []; + this.toolCallMap = {}; + } +} + +export default AGUIEventMapper; diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/events.ts b/packages/pro-components/chat/chatbot/core/adapters/agui/events.ts new file mode 100644 index 0000000000..2fa46112e3 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/events.ts @@ -0,0 +1,226 @@ +import { z } from 'zod'; + +import { MessageSchema, StateSchema } from './types'; + +export enum EventType { + TEXT_MESSAGE_START = 'TEXT_MESSAGE_START', + TEXT_MESSAGE_CONTENT = 'TEXT_MESSAGE_CONTENT', + TEXT_MESSAGE_END = 'TEXT_MESSAGE_END', + TEXT_MESSAGE_CHUNK = 'TEXT_MESSAGE_CHUNK', + THINKING_TEXT_MESSAGE_START = 'THINKING_TEXT_MESSAGE_START', + THINKING_TEXT_MESSAGE_CONTENT = 'THINKING_TEXT_MESSAGE_CONTENT', + THINKING_TEXT_MESSAGE_END = 'THINKING_TEXT_MESSAGE_END', + TOOL_CALL_START = 'TOOL_CALL_START', + TOOL_CALL_ARGS = 'TOOL_CALL_ARGS', + TOOL_CALL_END = 'TOOL_CALL_END', + TOOL_CALL_CHUNK = 'TOOL_CALL_CHUNK', + TOOL_CALL_RESULT = 'TOOL_CALL_RESULT', + THINKING_START = 'THINKING_START', + THINKING_END = 'THINKING_END', + STATE_SNAPSHOT = 'STATE_SNAPSHOT', + STATE_DELTA = 'STATE_DELTA', + MESSAGES_SNAPSHOT = 'MESSAGES_SNAPSHOT', + RAW = 'RAW', + CUSTOM = 'CUSTOM', + RUN_STARTED = 'RUN_STARTED', + RUN_FINISHED = 'RUN_FINISHED', + RUN_ERROR = 'RUN_ERROR', + STEP_STARTED = 'STEP_STARTED', + STEP_FINISHED = 'STEP_FINISHED', +} + +const BaseEventSchema = z.object({ + type: z.nativeEnum(EventType), + timestamp: z.number().optional(), + rawEvent: z.any().optional(), +}); + +export const TextMessageStartEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TEXT_MESSAGE_START), + messageId: z.string(), + role: z.literal('assistant'), +}); + +export const TextMessageContentEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TEXT_MESSAGE_CONTENT), + messageId: z.string(), + delta: z.string().refine((s) => s.length > 0, 'Delta must not be an empty string'), +}); + +export const TextMessageEndEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TEXT_MESSAGE_END), + messageId: z.string(), +}); + +export const TextMessageChunkEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TEXT_MESSAGE_CHUNK), + messageId: z.string().optional(), + role: z.literal('assistant').optional(), + delta: z.string().optional(), +}); + +export const ThinkingTextMessageStartEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.THINKING_TEXT_MESSAGE_START), +}); + +export const ThinkingTextMessageContentEventSchema = TextMessageContentEventSchema.omit({ + messageId: true, + type: true, +}).extend({ + type: z.literal(EventType.THINKING_TEXT_MESSAGE_CONTENT), +}); + +export const ThinkingTextMessageEndEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.THINKING_TEXT_MESSAGE_END), +}); + +export const ToolCallStartEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TOOL_CALL_START), + toolCallId: z.string(), + toolCallName: z.string(), + parentMessageId: z.string().optional(), +}); + +export const ToolCallArgsEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TOOL_CALL_ARGS), + toolCallId: z.string(), + delta: z.string(), +}); + +export const ToolCallEndEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TOOL_CALL_END), + toolCallId: z.string(), +}); + +export const ToolCallResultEventSchema = BaseEventSchema.extend({ + messageId: z.string(), + type: z.literal(EventType.TOOL_CALL_RESULT), + toolCallId: z.string(), + toolCallName: z.string().optional(), // todo: add to protocol + content: z.string(), + role: z.literal('tool').optional(), +}); + +export const ToolCallChunkEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.TOOL_CALL_CHUNK), + toolCallId: z.string().optional(), + toolCallName: z.string().optional(), + parentMessageId: z.string().optional(), + delta: z.string().optional(), +}); + +export const ThinkingStartEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.THINKING_START), + title: z.string().optional(), +}); + +export const ThinkingEndEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.THINKING_END), +}); + +export const StateSnapshotEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.STATE_SNAPSHOT), + snapshot: StateSchema, +}); + +export const StateDeltaEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.STATE_DELTA), + delta: z.array(z.any()), // JSON Patch (RFC 6902) +}); + +export const MessagesSnapshotEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.MESSAGES_SNAPSHOT), + messages: z.array(MessageSchema), +}); + +export const RawEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.RAW), + event: z.any(), + source: z.string().optional(), +}); + +export const CustomEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.CUSTOM), + name: z.string(), + value: z.any(), +}); + +export const RunStartedEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.RUN_STARTED), + threadId: z.string(), + runId: z.string(), +}); + +export const RunFinishedEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.RUN_FINISHED), + threadId: z.string(), + runId: z.string(), + result: z.any().optional(), +}); + +export const RunErrorEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.RUN_ERROR), + message: z.string(), + code: z.string().optional(), +}); + +export const StepStartedEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.STEP_STARTED), + stepName: z.string(), +}); + +export const StepFinishedEventSchema = BaseEventSchema.extend({ + type: z.literal(EventType.STEP_FINISHED), + stepName: z.string(), +}); + +export const EventSchemas = z.discriminatedUnion('type', [ + TextMessageStartEventSchema, + TextMessageContentEventSchema, + TextMessageEndEventSchema, + TextMessageChunkEventSchema, + ThinkingTextMessageStartEventSchema, + ThinkingTextMessageContentEventSchema, + ThinkingTextMessageEndEventSchema, + ToolCallStartEventSchema, + ToolCallArgsEventSchema, + ToolCallEndEventSchema, + ToolCallChunkEventSchema, + ToolCallResultEventSchema, + StateSnapshotEventSchema, + StateDeltaEventSchema, + MessagesSnapshotEventSchema, + RawEventSchema, + CustomEventSchema, + RunStartedEventSchema, + RunFinishedEventSchema, + RunErrorEventSchema, + StepStartedEventSchema, + StepFinishedEventSchema, +]); + +export type BaseEvent = z.infer; +export type TextMessageStartEvent = z.infer; +export type TextMessageContentEvent = z.infer; +export type TextMessageEndEvent = z.infer; +export type TextMessageChunkEvent = z.infer; +export type ThinkingTextMessageStartEvent = z.infer; +export type ThinkingTextMessageContentEvent = z.infer; +export type ThinkingTextMessageEndEvent = z.infer; +export type ToolCallStartEvent = z.infer; +export type ToolCallArgsEvent = z.infer; +export type ToolCallEndEvent = z.infer; +export type ToolCallChunkEvent = z.infer; +export type ToolCallResultEvent = z.infer; +export type ThinkingStartEvent = z.infer; +export type ThinkingEndEvent = z.infer; +export type StateSnapshotEvent = z.infer; +export type StateDeltaEvent = z.infer; +export type MessagesSnapshotEvent = z.infer; +export type RawEvent = z.infer; +export type CustomEvent = z.infer; +export type RunStartedEvent = z.infer; +export type RunFinishedEvent = z.infer; +export type RunErrorEvent = z.infer; +export type StepStartedEvent = z.infer; +export type StepFinishedEvent = z.infer; diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/index.ts b/packages/pro-components/chat/chatbot/core/adapters/agui/index.ts new file mode 100644 index 0000000000..af0808a70e --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/index.ts @@ -0,0 +1,224 @@ +/* eslint-disable max-classes-per-file */ +import type { AIMessageContent, SSEChunkData } from '../../type'; + +// export class AGUIEventMapper { +// private currentMessageId: string | null = null; + +// private currentContent: AIMessageContent[] = []; + +// mapEvent(chunk: SSEChunkData): AIMessageContent | AIMessageContent[] | null { +// const event = chunk.data; +// if (!event?.type) return null; + +// switch (event.type) { +// case EventType.TEXT_MESSAGE_START: +// this.currentMessageId = event.messageId; +// this.currentContent = [ +// { +// type: 'text', +// data: '', +// status: 'streaming', +// }, +// ]; +// return this.currentContent; + +// case EventType.TEXT_MESSAGE_CONTENT: +// if (!this.currentMessageId) return null; + +// // 更新文本内容 +// const textContent = this.currentContent.find((c) => c.type === 'text'); +// if (textContent && textContent.type === 'text') { +// textContent.data += event.delta; +// } +// return [...this.currentContent]; + +// case EventType.TEXT_MESSAGE_END: +// if (!this.currentMessageId) return null; + +// // 标记文本完成 +// const textContent = this.currentContent.find((c) => c.type === 'text'); +// if (textContent && textContent.type === 'text') { +// textContent.status = 'complete'; +// } +// return [...this.currentContent]; + +// case EventType.TOOL_CALL_START: +// this.currentContent.push({ +// type: 'tool_call', +// data: { +// name: event.toolCallName, +// arguments: '', +// }, +// status: 'pending', +// }); +// return [...this.currentContent]; + +// case EventType.TOOL_CALL_ARGS: +// const toolCall = this.currentContent.find((c) => c.type === 'tool_call' && c.data?.name === event.toolCallName); +// if (toolCall && toolCall.type === 'tool_call') { +// toolCall.data.arguments += event.delta; +// } +// return [...this.currentContent]; + +// case EventType.TOOL_CALL_RESULT: +// this.currentContent.push({ +// type: 'text', +// data: event.content, +// status: 'complete', +// }); +// return [...this.currentContent]; + +// case EventType.THINKING_START: +// this.currentContent.push({ +// type: 'thinking', +// data: { title: '思考中...' }, +// status: 'streaming', +// }); +// return [...this.currentContent]; + +// case EventType.STATE_SNAPSHOT: +// // 处理状态快照(需要特殊处理) +// return this.handleStateSnapshot(event.snapshot); + +// default: +// return null; +// } +// } + +// private handleStateSnapshot(snapshot: any): AIMessageContent[] { +// // 将快照转换为消息内容 +// return snapshot.messages.flatMap((msg: any) => { +// if (msg.role === 'assistant') { +// return msg.content.map((content: any) => ({ +// type: content.type, +// data: content.data, +// status: 'complete', +// })); +// } +// return []; +// }); +// } + +// reset() { +// this.currentMessageId = null; +// this.currentContent = []; +// } +// } + +export class AGUIEventMapper { + /** + * 将AG-UI事件转换为AIMessageContent + */ + mapEvent(chunk: SSEChunkData): AIMessageContent | AIMessageContent[] | null { + const event = chunk.data; + if (!event?.type) return null; + + switch (event.type) { + case 'TEXT_MESSAGE_START': + case 'TEXT_MESSAGE_CHUNK': + case 'TEXT_MESSAGE_END': + return { + type: 'markdown', + status: event.type === 'TEXT_MESSAGE_END' ? 'complete' : 'streaming', + data: event.data.content || event.data.text || '', + }; + + case 'TOOL_CALL_START': + case 'TOOL_CALL_CHUNK': + case 'TOOL_CALL_END': + return { + type: 'search', + data: { + title: event.data.toolName || 'Tool Call', + references: [], + }, + }; + + case 'RUN_ERROR': + return { + type: 'text', + data: event.data.error || event.data.message || 'Unknown error', + }; + + case 'CUSTOM': + // 处理自定义事件,尝试解析为通用格式 + if (event.data.type === 'thinking') { + return { + type: 'thinking', + data: { + text: event.data.content || event.data.text || '', + title: event.data.title, + }, + }; + } + + if (event.data.type === 'search') { + return { + type: 'search', + data: { + title: 'Search', + references: [], + }, + }; + } + + return { + type: 'text', + data: event.data.content || event.data.text || JSON.stringify(event.data), + }; + + default: + // 忽略生命周期事件(RUN_STARTED, RUN_FINISHED等) + return null; + } + } + + // private handleThinkingStart(event: any): ThinkingContent { + // return { + // type: 'thinking', + // data: { + // title: event.title || '思考中...', + // }, + // status: 'streaming', + // }; + // } + + // private handleThinkingEnd(event: any): ThinkingContent { + // return { + // type: 'thinking', + // status: 'complete', + // }; + // } + + // private handleRunStarted(event: any) {} + + // private handleRunFinished(event: any) {} + + // private handleRunError(event: any): ThinkingContent { + // return { + // type: 'thinking', + // data: { + // title: '处理出错', + // text: event.message || '未知错误', + // }, + // status: 'error', + // }; + // } + + // private handleStateSnapshot(event: any): SearchContent | null { + // if (!event.snapshot?.references) return null; + + // return { + // type: 'search', + // data: { + // title: '相关参考资料', + // references: event.snapshot.references.map((ref: any) => ({ + // title: ref.title, + // url: ref.url, + // content: ref.content, + // })), + // }, + // status: 'complete', + // }; + // } +} diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/types.ts b/packages/pro-components/chat/chatbot/core/adapters/agui/types.ts new file mode 100644 index 0000000000..9eee8d6a68 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/types.ts @@ -0,0 +1,106 @@ +import { z } from 'zod'; + +export const FunctionCallSchema = z.object({ + name: z.string(), + arguments: z.string(), +}); + +export const ToolCallSchema = z.object({ + id: z.string(), + type: z.literal('function'), + function: FunctionCallSchema, +}); + +export const BaseMessageSchema = z.object({ + id: z.string(), + role: z.string(), + content: z.string().optional(), + name: z.string().optional(), +}); + +export const DeveloperMessageSchema = BaseMessageSchema.extend({ + role: z.literal('developer'), + content: z.string(), +}); + +export const SystemMessageSchema = BaseMessageSchema.extend({ + role: z.literal('system'), + content: z.string(), +}); + +export const AssistantMessageSchema = BaseMessageSchema.extend({ + role: z.literal('assistant'), + content: z.string().optional(), + toolCalls: z.array(ToolCallSchema).optional(), +}); + +export const UserMessageSchema = BaseMessageSchema.extend({ + role: z.literal('user'), + content: z.string(), +}); + +export const ToolMessageSchema = z.object({ + id: z.string(), + content: z.string(), + role: z.literal('tool'), + toolCallId: z.string(), +}); + +export const MessageSchema = z.discriminatedUnion('role', [ + DeveloperMessageSchema, + SystemMessageSchema, + AssistantMessageSchema, + UserMessageSchema, + ToolMessageSchema, +]); + +export const RoleSchema = z.union([ + z.literal('developer'), + z.literal('system'), + z.literal('assistant'), + z.literal('user'), + z.literal('tool'), +]); + +export const ContextSchema = z.object({ + description: z.string(), + value: z.string(), +}); + +export const ToolSchema = z.object({ + name: z.string(), + description: z.string(), + parameters: z.any(), // JSON Schema for the tool parameters +}); + +export const RunAgentInputSchema = z.object({ + threadId: z.string(), + runId: z.string(), + state: z.any(), + messages: z.array(MessageSchema), + tools: z.array(ToolSchema), + context: z.array(ContextSchema), + forwardedProps: z.any(), +}); + +export const StateSchema = z.any(); + +export type ToolCall = z.infer; +export type FunctionCall = z.infer; +export type DeveloperMessage = z.infer; +export type SystemMessage = z.infer; +export type AssistantMessage = z.infer; +export type UserMessage = z.infer; +export type ToolMessage = z.infer; +export type Message = z.infer; +export type Context = z.infer; +export type Tool = z.infer; +export type RunAgentInput = z.infer; +export type State = z.infer; +export type Role = z.infer; + +export class AGUIError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/batch-client.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/batch-client.ts new file mode 100644 index 0000000000..0e7e100acf --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/batch-client.ts @@ -0,0 +1,63 @@ +import EventEmitter from '../utils/eventEmitter'; +import { LoggerManager } from '../utils/logger'; +import { ConnectionError, TimeoutError } from './errors'; + +/** + * 批量请求客户端(非流式) + */ +export class BatchClient extends EventEmitter { + private controller: AbortController | null = null; + + private logger = LoggerManager.getLogger(); + + /** + * 发送批量请求 + * @param endpoint API端点 + * @param request 请求参数 + * @param timeout 超时时间(毫秒) + * @returns 响应数据 + */ + async request(endpoint: string, request: RequestInit, timeout = 1000000): Promise { + // 中止上一个请求 + this.abort(); + + this.controller = new AbortController(); + const timeoutId = setTimeout(() => { + if (!this.controller?.signal.aborted) { + this.controller?.abort(); + } + this.emit('error', new TimeoutError(`Request timed out after ${timeout}ms`)); + }, timeout); + + try { + const response = await fetch(endpoint, { + ...request, + signal: this.controller.signal, + }); + + if (!response.ok) { + this.emit('error', new ConnectionError(`HTTP error! status: ${response.status}`)); + return null as T; + } + return (await response.json()) as T; + } catch (error) { + if (error.name !== 'AbortError') { + this.logger.error('Batch request failed:', error); + this.emit('error', error); + } + } finally { + clearTimeout(timeoutId); + this.controller = null; + } + } + + /** + * 中止当前请求 + */ + abort(): void { + if (this.controller) { + this.controller.abort(); + this.controller = null; + } + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/connection-manager.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/connection-manager.ts new file mode 100644 index 0000000000..281424f084 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/connection-manager.ts @@ -0,0 +1,88 @@ +import { LoggerManager } from '../utils/logger'; +import { TimeoutError } from './errors'; +import { ConnectionInfo, ConnectionStats, SSEConnectionState } from './types'; + +/** + * 连接管理器 - 直接使用全局Logger + */ +export class ConnectionManager { + private connectionStartTime = 0; + + private stats: ConnectionStats; + + private logger = LoggerManager.getLogger(); // 直接使用全局Logger + + constructor(private connectionId: string) { + this.stats = {}; + } + + /** + * 处理连接错误 + */ + handleConnectionError(error: Error): boolean { + this.logger.error(`Connection ${this.connectionId} error:`, error); + this.stats.lastError = error; + + if (error instanceof TimeoutError) { + this.logger.info('Timeout error occurred, no retry will be attempted'); + } + + this.cleanup(); + return false; + } + + /** + * 开始连接计时 + */ + startConnection(): void { + this.connectionStartTime = Date.now(); + this.logger.debug(`Connection ${this.connectionId} started`); + } + + /** + * 连接成功 + */ + onConnectionSuccess(): void { + const duration = Date.now() - this.connectionStartTime; + this.logger.info(`Connection established in ${duration}ms`); + this.stats.connectionTime = duration; + } + + /** + * 更新连接状态并记录统计信息 + */ + updateState(state: SSEConnectionState, error?: Error): void { + if (error) { + this.stats.lastError = error; + } + + this.logger.debug(`State updated to ${state}`); + } + + /** + * 获取连接信息 + */ + getConnectionInfo(): ConnectionInfo { + return { + id: this.connectionId, + retryCount: 0, + lastActivity: Date.now(), + stats: { ...this.stats }, + }; + } + + /** + * 获取连接统计 + */ + getStats(): ConnectionStats { + return { ...this.stats }; + } + + /** + * 清理资源 + */ + cleanup(): void { + this.stats = {}; + this.logger.debug(`Connection manager ${this.connectionId} cleaned up`); + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/errors.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/errors.ts new file mode 100644 index 0000000000..d483b038cc --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/errors.ts @@ -0,0 +1,50 @@ +/* eslint-disable max-classes-per-file */ +/** + * Enhanced error classes for SSE client + */ + +// 基础 SSE 错误类 +export class SSEError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly statusCode?: number, + public readonly isRetryable: boolean = false, + public readonly details?: any, + ) { + super(message); + this.name = 'SSEError'; + } +} + +// 连接错误 +export class ConnectionError extends SSEError { + constructor(message: string, statusCode?: number, details?: any) { + super(message, 'CONNECTION_ERROR', statusCode, true, details); + this.name = 'ConnectionError'; + } +} + +// 超时错误 +export class TimeoutError extends SSEError { + constructor(details?: any, message = '请求超时') { + super(message, 'TIMEOUT_ERROR', undefined, true, details); + this.name = 'TimeoutError'; + } +} + +// 解析错误 +export class ParseError extends SSEError { + constructor(message: string, details?: any) { + super(message, 'PARSE_ERROR', undefined, false, details); + this.name = 'ParseError'; + } +} + +// 验证错误 +export class ValidationError extends SSEError { + constructor(message: string, details?: any) { + super(message, 'VALIDATION_ERROR', undefined, false, details); + this.name = 'ValidationError'; + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/index.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/index.ts new file mode 100644 index 0000000000..5dfa8461a9 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/index.ts @@ -0,0 +1,22 @@ +/** + * Enhanced Server Module - 增强的 SSE 服务器模块 + * 提供完整的 SSE 客户端、连接管理和 LLM 服务功能 + */ + +// 导出所有类型 +// 默认导出(向后兼容) +import { LLMService } from './llm-service'; + +export * from './types'; + +// 导出错误类 +export * from './errors'; + +// 导出核心组件 + +export { ConnectionManager } from './connection-manager'; +export { EnhancedSSEClient } from './sse-client'; + +// 导出服务(保持向后兼容) +export { LLMService, type ILLMService } from './llm-service'; +export default LLMService; diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/llm-service.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/llm-service.ts new file mode 100644 index 0000000000..c8ce7dea4e --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/llm-service.ts @@ -0,0 +1,140 @@ +import type { AIMessageContent, ChatRequestParams, ChatServiceConfig, SSEChunkData } from '../type'; +import { LoggerManager } from '../utils/logger'; +import { BatchClient } from './batch-client'; +import { EnhancedSSEClient } from './sse-client'; + +// 与原有接口保持兼容 +export interface ILLMService { + /** + * 处理批量请求(非流式) + */ + handleBatchRequest( + params: ChatRequestParams, + config: ChatServiceConfig, + ): Promise; + + /** + * 处理流式请求 + */ + handleStreamRequest(params: ChatRequestParams, config: ChatServiceConfig): Promise; +} + +/** + * Enhanced LLM Service with error handling and connection management + */ +export class LLMService implements ILLMService { + // 使用接口确保类型安全 + private sseClient: EnhancedSSEClient; + + private batchClient: BatchClient | null = null; + + private isDestroyed = false; + + private logger = LoggerManager.getLogger(); + + constructor(private config: ChatServiceConfig) { + this.logger.info('Enhanced LLM Service initialized'); + } + + /** + * 处理批量请求(非流式) + */ + async handleBatchRequest( + params: ChatRequestParams, + config: ChatServiceConfig, + ): Promise { + // 确保只有一个客户端实例 + this.batchClient = this.batchClient || new BatchClient(); + this.batchClient.on('error', (error) => { + config.onError?.(error); + }); + + const req = (await config.onRequest?.(params)) || params; + + try { + const data = await this.batchClient.request( + config.endpoint, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...req.headers, + }, + body: req.body, + }, + config.timeout, // 现在timeout属性已存在 + ); + if (data) { + return config.onComplete?.(false, req, data); + } + } catch (error) { + config.onError?.(error); + throw error; + } + } + + /** + * 处理流式请求 + */ + async handleStreamRequest(params: ChatRequestParams, config: ChatServiceConfig): Promise { + if (!config.endpoint) return; + this.sseClient = new EnhancedSSEClient(config.endpoint); + + const req = (await config.onRequest?.(params)) || {}; + + // 设置事件处理器 + this.sseClient.on('start', (chunk) => { + config.onStart?.(chunk); + }); + + this.sseClient.on('message', (msg) => { + config.onMessage?.(msg as SSEChunkData); + }); + + this.sseClient.on('error', (error) => { + config.onError?.(error); + }); + + this.sseClient.on('complete', (isAborted) => { + config.onComplete?.(isAborted, req); + }); + + await this.sseClient.connect(req); + } + + /** + * 关闭所有客户端连接 + */ + closeConnect(): void { + if (this.sseClient) { + this.sseClient.abort(); + this.sseClient = null; + } + if (this.batchClient) { + this.batchClient?.abort(); + this.batchClient = null; + } + } + + /** + * 获取连接统计 + */ + getSSEStats(): { id: string; status: string; info: any } | null { + if (!this.sseClient) return null; + + return { + id: this.sseClient.connectionId, + status: this.sseClient.getStatus(), + info: this.sseClient.getInfo(), + }; + } + + /** + * 销毁服务 + */ + async destroy(): Promise { + this.isDestroyed = true; + this.closeConnect(); + this.logger.info('Enhanced LLM Service destroyed'); + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/sse-client.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/sse-client.ts new file mode 100644 index 0000000000..e6e779bd25 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/sse-client.ts @@ -0,0 +1,295 @@ +/* eslint-disable no-await-in-loop, max-classes-per-file */ +import EventEmitter from '../utils/eventEmitter'; +import { LoggerManager } from '../utils/logger'; +import { ConnectionManager } from './connection-manager'; +import { ConnectionError, TimeoutError } from './errors'; +import { SSEEvent, SSEParser } from './sse-parser'; +import { ConnectionInfo, DEFAULT_SSE_CONFIG, SSEClientConfig, SSEConnectionState, StateChangeEvent } from './types'; + +/** + * Enhanced SSE Client + * 采用分层设计,分离了连接管理、状态管理、事件解析等职责 + */ +export class EnhancedSSEClient extends EventEmitter { + public readonly connectionId: string; + + private state = SSEConnectionState.DISCONNECTED; + + private controller?: AbortController | null; + + private reader?: ReadableStreamDefaultReader; + + private connectionManager: ConnectionManager; + + private parser: SSEParser; + + private timeoutTimer?: ReturnType; + + private config: SSEClientConfig; + + private logger = LoggerManager.getLogger(); + + private url: string; + + private connectionInfo: ConnectionInfo; + + private firstTokenReceived = false; + + constructor(url: string) { + super(); + this.url = url; + this.connectionId = this.generateConnectionId(); + this.logger = LoggerManager.getLogger(); + this.connectionManager = new ConnectionManager(this.connectionId); + + // 初始化 SSE 解析器 + this.parser = new SSEParser(); + this.parser.onMessage = (event: SSEEvent) => { + this.emit('message', event); + }; + + this.connectionInfo = { + id: this.connectionId, + url, + state: this.state, + createdAt: Date.now(), + retryCount: 0, + lastActivity: Date.now(), + stats: {}, + }; + + this.setupInternalEventHandlers(); + } + + /** + * 连接 SSE 服务 + */ + async connect(config: SSEClientConfig): Promise { + if (this.state === SSEConnectionState.CONNECTED || this.state === SSEConnectionState.CONNECTING) { + return; + } + + this.config = { + ...DEFAULT_SSE_CONFIG, + ...config, + headers: { + ...DEFAULT_SSE_CONFIG.headers, + ...config.headers, + }, + }; + + this.setState(SSEConnectionState.CONNECTING); + this.connectionManager.startConnection(); + + try { + await this.establishConnection(); + this.setState(SSEConnectionState.CONNECTED); + this.connectionManager.onConnectionSuccess(); + await this.readStream(); + } catch (error) { + this.handleConnectionError(error as Error); + } + } + + /** + * 关闭连接 + */ + async abort(): Promise { + if (this.state === SSEConnectionState.DISCONNECTED || this.state === SSEConnectionState.CLOSING) { + return; + } + + this.setState(SSEConnectionState.CLOSING); + + try { + if (this.reader) { + await this.reader.cancel(); + this.reader = undefined; + } + + if (this.controller && !this.controller.signal.aborted) { + this.controller.abort(); + } + + this.connectionManager.cleanup(); + this.resetParser(); + this.emit('complete', true); + } finally { + this.clearTimeouts(); + this.setState(SSEConnectionState.CLOSED); + } + } + + /** + * 获取连接状态 + */ + getStatus(): SSEConnectionState { + return this.state; + } + + /** + * 获取连接信息 + */ + getInfo(): ConnectionInfo { + return { + ...this.connectionInfo, + ...this.connectionManager.getConnectionInfo(), + }; + } + + /** + * 建立连接 + */ + private async establishConnection(): Promise { + this.controller = new AbortController(); + + // 设置超时 + if (this.config.timeout && this.config.timeout > 0) { + this.timeoutTimer = setTimeout(() => { + if (!this.controller?.signal.aborted) { + this.controller?.abort(); + } + this.emit('error', new TimeoutError(`Request timed out after ${this.config.timeout}ms`)); + }, this.config.timeout); + } + + try { + const response = await fetch(this.url, { + ...this.config, + signal: this.controller.signal, + headers: { + ...this.config.headers, + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + }); + + if (!response.body || !response.ok) { + this.emit( + 'error', + new ConnectionError(`HTTP ${response.status}: ${response.statusText}`, response.status, response), + ); + return; + } + this.reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + } catch (error) { + if (error.name !== 'AbortError') { + this.logger.error('sse request failed:', error); + this.emit('error', error); + } + } finally { + this.clearTimeouts(); + this.controller = null; + } + } + + /** + * 读取流数据 + */ + private async readStream(): Promise { + try { + while (this.state === SSEConnectionState.CONNECTED && this.reader) { + // eslint-disable no-await-in-loop + const { done, value } = await this.reader.read(); + + if (done) { + this.logger.info(`Connection ${this.connectionId} stream ended normally`); + this.emit('complete', false); // 发出流结束事件 + this.setState(SSEConnectionState.DISCONNECTED); + return; + } + + // 更新活动时间 + this.connectionInfo.lastActivity = Date.now(); + + // 直接解析SSE数据 + this.parseSSEData(value); + } + } catch (error) { + if (!this.controller?.signal.aborted) { + this.logger.error(`Stream reading error for ${this.connectionId}:`, error); + this.handleConnectionError(error as Error); + } else { + this.logger.debug(`Stream reading stopped for ${this.connectionId} (aborted)`); + } + } + } + + /** + * 解析SSE数据 + */ + private parseSSEData(chunk: string): void { + // 使用独立的 SSE 解析器处理数据 + this.parser.parse(chunk); + if (!this.firstTokenReceived) { + this.firstTokenReceived = true; + this.emit('start', chunk); // 派发start事件 + } + } + + /** + * 简化的错误处理 + */ + private handleConnectionError(error: Error) { + this.connectionInfo.error = error; + this.connectionManager.handleConnectionError(error); + this.setState(SSEConnectionState.ERROR); + this.emit('error', error); + } + + /** + * 清理超时定时器 + */ + private clearTimeouts(): void { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = undefined; + } + } + + /** + * 设置连接状态 + */ + private setState(newState: SSEConnectionState) { + const oldState = this.state; + this.state = newState; + this.connectionInfo.state = newState; + + const stateChangeEvent: StateChangeEvent = { + connectionId: this.connectionId, + from: oldState, + to: newState, + timestamp: Date.now(), + }; + + this.emit('stateChange', stateChangeEvent); + this.logger.debug(`Connection ${this.connectionId} state: ${oldState} -> ${newState}`); + } + + /** + * 重置解析器状态 + */ + private resetParser(): void { + this.parser.reset(); + } + + /** + * 设置内部事件处理器 + */ + private setupInternalEventHandlers(): void { + this.on('error', (error) => { + this.logger.error(`SSE Client ${this.connectionId} error:`, error); + }); + + this.on('complete', (isAborted) => { + this.logger.info(`SSE Client ${this.connectionId} completed, aborted: ${isAborted}`); + }); + } + + /** + * 生成连接ID + */ + private generateConnectionId(): string { + return `sse_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/sse-parser.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/sse-parser.ts new file mode 100644 index 0000000000..858d4895dc --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/sse-parser.ts @@ -0,0 +1,149 @@ +import { LoggerManager } from '../utils/logger'; + +/** + * SSE 事件接口 + */ +export interface SSEEvent { + event?: string; + data?: any; + id?: string; +} + +/** + * SSE 事件流解析器 + */ +export class SSEParser { + private eventBuffer = ''; + + private currentEvent: { event?: string; data?: string; id?: string } = {}; + + private logger = LoggerManager.getLogger(); + + // 事件回调函数 + onMessage?: (msg: SSEEvent) => void; + + /** + * 解析SSE数据块 + */ + parse(chunk: string): void { + this.eventBuffer += chunk; + + // 循环处理,直到缓冲区中再也找不到完整的行 + let newlineIndex; + // eslint-disable-next-line no-cond-assign + while ((newlineIndex = this.eventBuffer.indexOf('\n')) !== -1) { + // 提取一行(包含 \r 如果有的话) + const line = this.eventBuffer.slice(0, newlineIndex).replace(/\r$/, ''); + + // 从缓冲区移除已处理的行和换行符 + this.eventBuffer = this.eventBuffer.slice(newlineIndex + 1); + + // 处理这一行 + this.processSSELine(line); + } + } + + /** + * 处理SSE行数据 + */ + private processSSELine(line: string): void { + if (line === '') { + // 空行表示事件结束 + this.emitCurrentEvent(); + return; + } + + const colonIndex = line.indexOf(':'); + if (colonIndex === 0) { + // 注释行,忽略 + return; + } + + let field: string; + let value: string; + + if (colonIndex === -1) { + field = line.trim(); + value = ''; + } else { + field = line.slice(0, colonIndex).trim(); + value = line.slice(colonIndex + 1).replace(/^ /, ''); // 移除开头空格 + } + + // 处理SSE字段 + switch (field) { + case 'event': + this.currentEvent.event = value; + break; + case 'data': + if (this.currentEvent.data === undefined) { + this.currentEvent.data = value; + } else { + this.currentEvent.data += `\n${value}`; + } + break; + case 'id': + this.currentEvent.id = value; + break; + default: + // 忽略其他字段 + break; + } + } + + /** + * 发送当前事件 + */ + private emitCurrentEvent(): void { + if (this.currentEvent.data !== undefined && this.onMessage) { + try { + // 尝试解析JSON,失败则保持原始字符串 + let data: any; + try { + data = JSON.parse(this.currentEvent.data); + } catch { + data = this.currentEvent.data; + } + + this.onMessage({ + event: this.currentEvent.event || '', + data, + }); + } catch (error) { + this.logger.error('Error emitting event:', error); + } + } + + // 清理当前事件 + this.currentEvent = {}; + } + + /** + * 重置解析器状态 + */ + reset(): void { + this.eventBuffer = ''; + this.currentEvent = {}; + } + + /** + * 获取当前缓冲区大小 + */ + getBufferSize(): number { + return this.eventBuffer.length; + } + + /** + * 获取当前事件状态 + */ + getCurrentEvent(): { event?: string; data?: string; id?: string } { + return { ...this.currentEvent }; + } + + /** + * 检查是否有未完成的事件 + */ + hasIncompleteEvent(): boolean { + return this.currentEvent.data !== undefined; + } +} diff --git a/packages/pro-components/chat/chatbot/core/enhanced-server/types.ts b/packages/pro-components/chat/chatbot/core/enhanced-server/types.ts new file mode 100644 index 0000000000..e28146676d --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/enhanced-server/types.ts @@ -0,0 +1,78 @@ +/** + * Enhanced server types for SSE client and LLM service + */ + +// 连接状态枚举 +export enum SSEConnectionState { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + RECONNECTING = 'reconnecting', + CLOSING = 'closing', + CLOSED = 'closed', + ERROR = 'error', +} + +// SSE 客户端配置 +export interface SSEClientConfig extends Omit { + timeout?: number; +} + +// 事件接口 +export interface SSEEvent { + event?: string; + data?: any; + id?: string; + retry?: number; + timestamp?: number; +} + +// 连接信息 +export interface ConnectionInfo { + id: string; + retryCount: number; + lastActivity: number; + state?: SSEConnectionState; // 可选状态 + url?: string; // 可选URL + createdAt?: number; // 可选创建时间 + error?: Error; // 可选错误信息 + stats: ConnectionStats; // 添加必需的统计信息属性 +} + +// 简化的连接状态信息 +export interface ConnectionStatus { + id: string; + state: SSEConnectionState; + error?: Error; + lastActivity?: number; +} + +// 简化的连接统计 +export interface ConnectionStats { + lastError?: Error; + connectionTime?: number; +} + +// 状态变化事件 +export interface StateChangeEvent { + connectionId: string; + from: SSEConnectionState; + to: SSEConnectionState; + timestamp: number; +} + +// 心跳事件 +export interface HeartbeatEvent { + connectionId: string; + timestamp: number; +} + +export const DEFAULT_SSE_CONFIG: SSEClientConfig = { + timeout: 0, + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + }, +}; diff --git a/packages/pro-components/chat/chatbot/core/index.ts b/packages/pro-components/chat/chatbot/core/index.ts new file mode 100644 index 0000000000..04e88ee4e3 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/index.ts @@ -0,0 +1,309 @@ +/* eslint-disable class-methods-use-this */ +import { AGUIEventMapper } from './adapters/agui/agui-event-mapper'; +import { MessageStore } from './store/message'; +import { LLMService } from './enhanced-server'; +import MessageProcessor from './processor'; +import type { + AIContentChunkUpdate, + AIMessageContent, + ChatMessagesData, + ChatMessageSetterMode, + ChatRequestParams, + ChatServiceConfig, + ChatServiceConfigSetter, + SSEChunkData, + SystemMessage, +} from './type'; +import { isAIMessage } from './utils'; +import { EventType } from './adapters/agui/events'; +import { handleError } from '../../../../../../tdesign-web-components/src/_common/js/upload/main'; + +export interface IChatEngine { + init(config?: any, messages?: ChatMessagesData[]): void; + sendUserMessage(params: ChatRequestParams): Promise; + regenerateAIMessage(keepVersion?: boolean): Promise; + abortChat(): Promise; + setMessages(messages: ChatMessagesData[], mode?: ChatMessageSetterMode): void; + registerMergeStrategy(type: T['type'], handler: (chunk: T, existing?: T) => T): void; + + // 属性访问 + get messageStore(): MessageStore; +} + +export default class ChatEngine implements IChatEngine { + public messageStore: MessageStore; + + private processor: MessageProcessor; + + private llmService: LLMService; + + private config: ChatServiceConfig; + + private lastRequestParams: ChatRequestParams | undefined; + + private stopReceive = false; + + private aguiEventMapper: AGUIEventMapper | null = null; + + constructor() { + this.processor = new MessageProcessor(); + this.messageStore = new MessageStore(); + } + + public init(configSetter: ChatServiceConfigSetter, initialMessages?: ChatMessagesData[]) { + this.messageStore.initialize(this.convertMessages(initialMessages)); + this.config = typeof configSetter === 'function' ? configSetter() : configSetter || {}; + this.llmService = new LLMService(this.config); + // 初始化AG-UI事件映射器 + if (this.config.protocol === 'agui') { + this.aguiEventMapper = new AGUIEventMapper(); + } + } + + public async sendUserMessage(requestParams: ChatRequestParams) { + const { prompt, attachments } = requestParams; + const userMessage = this.processor.createUserMessage(prompt, attachments); + const aiMessage = this.processor.createAssistantMessage(); + this.messageStore.createMultiMessages([userMessage, aiMessage]); + const params = { + prompt, + attachments, + messageID: aiMessage.id, + }; + this.sendRequest(params); + } + + public async sendSystemMessage(msg: string) { + this.messageStore.createMessage({ + role: 'system', + content: [ + { + type: 'text', + data: msg, + }, + ], + } as SystemMessage); + } + + public async abortChat() { + this.stopReceive = true; + + if (this.config?.onAbort) { + await this.config.onAbort(); + } + + try { + this.llmService.closeConnect(); + if (!this.config.stream) { + // 只有在批量模式下才删除最后一条AI消息 + if (this.messageStore.lastAIMessage?.id) { + this.messageStore.removeMessage(this.messageStore.lastAIMessage.id); + } + } + } catch (error) { + console.warn('Error during service cleanup:', error); + } + } + + public registerMergeStrategy( + type: T['type'], // 使用类型中定义的type字段作为参数类型 + handler: (chunk: T, existing?: T) => T, + ) { + this.processor.registerHandler(type, handler); + } + + public setMessages(messages: ChatMessagesData[], mode: ChatMessageSetterMode = 'replace') { + this.messageStore.setMessages(messages, mode); + } + + // 用户触发重新生成 -> 检查最后一条AI消息 -> + // -> keepVersion=false: 删除旧消息 -> 创建新消息 -> 重新请求 + // -> keepVersion=true: 保留旧消息 -> 创建分支消息 -> 重新请求 + public async regenerateAIMessage(keepVersion = false) { + const { lastAIMessage, lastUserMessage } = this.messageStore; + if (!lastAIMessage) return; + + if (!this.lastRequestParams) { + // 应对历史消息也有重新生成的情况 + const { content, id } = lastUserMessage; + this.lastRequestParams = { + prompt: content.filter((c) => c.type === 'text')[0].data, + attachments: content.filter((c) => c.type === 'attachment')?.[0]?.data, + messageID: id, + }; + } + if (!keepVersion) { + // 删除最后一条AI消息 + this.messageStore.removeMessage(lastAIMessage.id); + } else { + // todo: 保留历史版本,创建新分支 + this.messageStore.createMessageBranch(lastAIMessage.id); + } + + // 创建新的AI消息 + const newAIMessage = this.processor.createAssistantMessage(); + this.messageStore.createMessage(newAIMessage); + + // 重新发起请求 + const params = { + ...this.lastRequestParams, + messageID: newAIMessage.id, + }; + + await this.sendRequest(params); + } + + public async sendRequest(params: ChatRequestParams) { + const { messageID: id } = params; + try { + if (this.config.stream) { + // 处理sse流式响应模式 + this.stopReceive = false; + await this.handleStreamRequest(params); + } else { + // 处理批量响应模式 + await this.handleBatchRequest(params); + } + this.lastRequestParams = params; + } catch (error) { + this.setMessageStatus(id, 'error'); + throw error; + } + } + + private async handleBatchRequest(params: ChatRequestParams) { + const id = params.messageID; + this.setMessageStatus(id, 'pending'); + const result = await this.llmService.handleBatchRequest(params, this.config); + if (result) { + this.processMessageResult(id, result); + this.setMessageStatus(id, 'complete'); + } else { + this.setMessageStatus(id, 'error'); + } + } + + private handleError(id, error: any) { + this.setMessageStatus(id, 'error'); + this.config.onError?.(error); + } + + private handleComplete(id, isAborted: boolean, params: ChatRequestParams, chunk?: any) { + // 所有消息内容块都失败才算消息体失败 + const allContentFailed = this.messageStore.messages.every((content) => content.status === 'error'); + // eslint-disable-next-line no-nested-ternary + this.setMessageStatus(id, isAborted ? 'stop' : allContentFailed ? 'error' : 'complete'); + + // 返回空数组以满足类型要求 + return this.config.onComplete?.(isAborted, params, chunk); + } + + private async handleStreamRequest(params: ChatRequestParams) { + const id = params.messageID; + const isAGUI = this.config.protocol === 'agui'; + this.setMessageStatus(id, 'streaming'); // todo: 这里应该在建立连接后在streaming + await this.llmService.handleStreamRequest(params, { + ...this.config, + onStart: (chunk) => { + if (!isAGUI) this.config.onStart?.(chunk); + }, + onMessage: (chunk: SSEChunkData) => { + if (this.stopReceive) return null; + let result = null; + if (this.config.onMessage) { + result = this.config.onMessage(chunk, this.messageStore.getMessageByID(id)); + } + // 统一处理 AG-UI 协议 + if (isAGUI && this.aguiEventMapper) { + const event = chunk.data; + if (!event?.type) return null; + switch (event.type) { + case EventType.RUN_STARTED: + this.config.onStart?.(event); + break; + case EventType.RUN_FINISHED: + this.handleComplete(id, false, params, event); + break; + case EventType.RUN_ERROR: + this.handleError(id, event); + break; + } + // 如果用户未处理,使用默认映射器 + if (!result) { + result = this.aguiEventMapper.mapEvent(chunk); + } + } + // 统一处理结果 + this.processMessageResult(id, result); + return result; + }, + onError: (error) => { + this.handleError(id, error); + }, + onComplete: (isAborted) => { + if (!isAborted && !isAGUI) { + return this.handleComplete(id, isAborted, params); + } + }, + }); + } + + /** + * 统一处理消息结果 + * 支持单个内容块、多个内容块和增量更新 + */ + private processMessageResult(messageId: string, result: AIMessageContent | AIMessageContent[] | null) { + if (!result) return; + + if (Array.isArray(result)) { + // 处理多个内容块 + this.messageStore.updateMultipleContents(messageId, result); + } else { + // 处理单个内容块 + this.processContentUpdate(messageId, result); + } + } + + private convertMessages(messages?: ChatMessagesData[]) { + if (!messages) return { messageIds: [], messages: [] }; + + return { + messageIds: messages.map((msg) => msg.id), + messages, + }; + } + + private setMessageStatus(messageId: string, status: ChatMessagesData['status']) { + this.messageStore.setMessageStatus(messageId, status); + } + + // 处理内容更新逻辑 + private processContentUpdate(messageId: string, rawChunk: AIContentChunkUpdate) { + const message = this.messageStore.messages.find((m) => m.id === messageId); + if (!message || !isAIMessage(message)) return; + + // // 只需要处理最后一个内容快 + // const lastContent = message.content.at(-1); + // const processed = this.processor.processContentUpdate(lastContent, rawChunk); + // this.messageStore.appendContent(messageId, processed); + + let targetIndex; + // 作为新的内容块追加 + if (rawChunk?.strategy === 'append') { + targetIndex = -1; + } else { + // 合并/替换到现有同类型内容中 + targetIndex = message.content.findLastIndex((content) => content.type === rawChunk.type); + } + + console.log('targetIndex', targetIndex, rawChunk.type); + const processed = this.processor.processContentUpdate( + targetIndex !== -1 ? message.content[targetIndex] : undefined, + rawChunk, + ); + + this.messageStore.appendContent(messageId, processed, targetIndex); + } +} + +export * from './utils'; diff --git a/packages/pro-components/chat/chatbot/core/processor/index.ts b/packages/pro-components/chat/chatbot/core/processor/index.ts new file mode 100644 index 0000000000..7668aff3c0 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/processor/index.ts @@ -0,0 +1,157 @@ +import { + AIMessage, + AttachmentItem, + ChatMessagesData, + UserMessage, + type AIMessageContent, + type ImageContent, + type MarkdownContent, + type SearchContent, + type TextContent, + type ThinkingContent, +} from '../type'; + +export default class MessageProcessor { + private contentHandlers: Map any> = new Map(); + + constructor() { + this.registerDefaultHandlers(); + } + + public createUserMessage(content: string, attachments?: AttachmentItem[]): ChatMessagesData { + const messageContent: UserMessage['content'] = [ + { + type: 'text', + data: content, + }, + ]; + + if (attachments?.length) { + messageContent.push({ + type: 'attachment', + data: attachments, + }); + } + + return { + id: this.generateID(), + role: 'user', + status: 'complete', + datetime: `${Date.now()}`, + content: messageContent, + }; + } + + public createAssistantMessage(): AIMessage { + // 创建初始助手消息 + return { + id: this.generateID(), + role: 'assistant', + status: 'pending', + datetime: `${Date.now()}`, + content: [], + }; + } + + // 处理内容更新 + public processContentUpdate(lastContent: AIMessageContent | undefined, newChunk: AIMessageContent): AIMessageContent { + const handler = this.contentHandlers.get(newChunk.type); + if (!lastContent) { + return { + ...newChunk, + status: newChunk?.status || 'streaming', + }; + } + // 如果有注册的处理器且类型匹配 + if (handler && lastContent?.type === newChunk.type) { + return handler(newChunk, lastContent); + } + // 没有处理器时的默认合并逻辑 + return { + ...(lastContent || {}), + ...newChunk, + status: newChunk?.status || 'streaming', + }; + } + + // 通用处理器注册方法 + public registerHandler( + type: T['type'], // 使用类型中定义的type字段作为参数类型 + handler: (chunk: T, existing?: T) => T, + ) { + this.contentHandlers.set(type, handler); + } + + private generateID() { + return `msg_${Date.now()}_${Math.floor(Math.random() * 90000) + 10000}`; + } + + // 注册默认支持的内容处理器 + private registerDefaultHandlers() { + this.registerTextHandlers(); + this.registerThinkingHandler(); + this.registerImageHandler(); + this.registerSearchHandler(); + } + + // 通用处理器工厂 + private createContentHandler( + mergeData: (existing: T['data'], incoming: T['data']) => T['data'], + ): (chunk: T, existing?: T) => T { + return (chunk: T, existing?: T): T => { + if (existing?.type === chunk.type) { + return { + ...existing, + data: mergeData(existing.data, chunk.data), + status: chunk.status || 'streaming', + }; + } + return { + ...chunk, + data: chunk.data, + status: chunk.status || 'streaming', + }; + }; + } + + // 文本类处理器(text/markdown) + private registerTextHandlers() { + // 创建类型安全的处理器 + const createTextHandler = () => + this.createContentHandler((existing: string, incoming: string) => existing + incoming); + + this.registerHandler('text', createTextHandler()); + this.registerHandler('markdown', createTextHandler()); + } + + // 思考过程处理器 + private registerThinkingHandler() { + this.registerHandler( + 'thinking', + this.createContentHandler((existing, incoming) => ({ + ...existing, + ...incoming, + text: (existing?.text || '') + (incoming?.text || ''), + })), + ); + } + + // 图片处理器 + private registerImageHandler() { + this.registerHandler( + 'image', + this.createContentHandler((existing, incoming) => ({ ...existing, ...incoming })), + ); + } + + // 搜索处理器 + private registerSearchHandler() { + this.registerHandler( + 'search', + this.createContentHandler((existing, incoming) => ({ + ...existing, + ...incoming, + })), + ); + } +} diff --git a/packages/pro-components/chat/chatbot/core/store/message.ts b/packages/pro-components/chat/chatbot/core/store/message.ts new file mode 100644 index 0000000000..a15224c317 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/store/message.ts @@ -0,0 +1,234 @@ +import type { + AIMessage, + AIMessageContent, + ChatMessagesData, + ChatMessageSetterMode, + ChatMessageStatus, + ChatMessageStore, + UserMessage, +} from '../type'; +import { isAIMessage, isUserMessage } from '../utils'; +import ReactiveState from './reactiveState'; + +// 专注消息生命周期管理 +export class MessageStore extends ReactiveState { + initialize(initialState?: Partial) { + super.initialize({ + messageIds: [], + messages: [], + ...initialState, + }); + } + + createMessage(message: ChatMessagesData) { + const { id } = message; + this.setState((draft) => { + draft.messageIds.push(id); + draft.messages.push(message); + }); + } + + createMultiMessages(messages: ChatMessagesData[]) { + this.setState((draft) => { + messages.forEach((msg) => { + draft.messageIds.push(msg.id); + }); + draft.messages.push(...messages); + }); + } + + setMessages(messages: ChatMessagesData[], mode: ChatMessageSetterMode = 'replace') { + this.setState((draft) => { + if (mode === 'replace') { + draft.messageIds = messages.map((msg) => msg.id); + draft.messages = [...messages]; + } else if (mode === 'prepend') { + draft.messageIds = [...messages.map((msg) => msg.id), ...draft.messageIds]; + draft.messages = [...messages, ...draft.messages]; + } else { + draft.messageIds.push(...messages.map((msg) => msg.id)); + draft.messages.push(...messages); + } + }); + } + + // 追加内容到指定类型的content + appendContent(messageId: string, processedContent: AIMessageContent, targetIndex = -1) { + this.setState((draft) => { + const message = draft.messages.find((m) => m.id === messageId); + if (!message || !isAIMessage(message)) return; + + if (targetIndex >= 0 && targetIndex < message.content.length) { + // 合并到指定位置 + message.content[targetIndex] = processedContent; + } else { + // 添加新内容块 + message.content.push(processedContent); + } + + this.updateMessageStatusByContent(message); + }); + } + + // 完整替换消息的content数组 + replaceContent(messageId: string, processedContent: AIMessageContent[]) { + this.setState((draft) => { + const message = draft.messages.find((m) => m.id === messageId); + if (!message || !isAIMessage(message)) return; + message.content = processedContent; + }); + } + + // 更新消息整体状态 + setMessageStatus(messageId: string, status: ChatMessagesData['status']) { + this.setState((draft) => { + const message = draft.messages.find((m) => m.id === messageId); + if (message) { + message.status = status; + if (isAIMessage(message) && message.content.length > 0) { + message.content.at(-1).status = status; + } + } + }); + } + + // 为消息设置扩展属性 + setMessageExt(messageId: string, attr = {}) { + this.setState((draft) => { + const message = draft.messages.find((m) => m.id === messageId); + if (message) { + message.ext = { ...message.ext, ...attr }; + } + }); + } + + clearHistory() { + this.setState((draft) => { + draft.messageIds = []; + draft.messages = []; + }); + } + + // 删除指定消息 + removeMessage(messageId: string) { + this.setState((draft) => { + // 从ID列表删除 + const idIndex = draft.messageIds.indexOf(messageId); + if (idIndex !== -1) { + draft.messageIds.splice(idIndex, 1); + } + + // 从消息列表删除 + draft.messages = draft.messages.filter((msg) => msg.id !== messageId); + }); + } + + // 创建消息分支(用于保留历史版本) + createMessageBranch(messageId: string) { + const original = this.getState().messages.find((m) => m.id === messageId); + if (!original) return; + + // 克隆消息并生成新ID + const branchedMessage = { + ...original, + content: original.content.map((c) => ({ ...c })), + }; + + this.createMessage(branchedMessage); + } + + get messages() { + return this.getState().messages; + } + + getMessageByID(id: string) { + return this.getState().messages.find((m) => m.id === id); + } + + get currentMessage(): ChatMessagesData { + const { messages } = this.getState(); + return messages.at(-1); + } + + get lastAIMessage(): AIMessage | undefined { + const { messages } = this.getState(); + const aiMessages = messages.filter((msg) => isAIMessage(msg)); + return aiMessages.at(-1); + } + + get lastUserMessage(): UserMessage | undefined { + const { messages } = this.getState(); + const userMessages = messages.filter((msg) => isUserMessage(msg)); + return userMessages.at(-1); + } + + private resolvedStatus(content: AIMessageContent, status: ChatMessageStatus): ChatMessageStatus { + return typeof content.status === 'function' ? content.status(status) : content.status; + } + + // 更新消息整体状态 + private updateMessageStatusByContent(message: AIMessage) { + // 优先处理错误状态 + if (message.content.some((c) => c.status === 'error')) { + message.status = 'error'; + message.content.forEach((content) => { + content.status = this.resolvedStatus(content, 'streaming') ? 'stop' : content.status; + }); + return; + } + + // 非最后一个内容块如果不是error|stop, 则设为content.status|complete + message.content + .slice(0, -1) // 获取除最后一个元素外的所有内容 + .forEach((content) => { + if (content.status !== 'error' && content.status !== 'stop') { + content.status = this.resolvedStatus(content, 'complete'); + } + }); + + // 检查是否全部完成 + const allComplete = message.content.every( + (c) => c.status === 'complete' || c.status === 'stop', // 包含停止状态 + ); + + message.status = allComplete ? 'complete' : 'streaming'; + } + + /** + * 更新多个内容块 + * @param messageId 消息ID + * @param contents 要更新的内容块数组 + */ + updateMultipleContents(messageId: string, contents: AIMessageContent[]) { + this.setState((draft) => { + const message = draft.messages.find((m) => m.id === messageId); + if (!message || !isAIMessage(message)) return; + + // 更新或添加每个内容块 + contents.forEach((content) => { + const existingIndex = message.content.findIndex((c) => c.id === content.id || c.type === content.type); + + if (existingIndex >= 0) { + // 更新现有内容块 + message.content[existingIndex] = { + ...message.content[existingIndex], + ...content, + }; + } else { + // 添加新内容块 + message.content.push(content); + } + }); + + // 更新消息状态 + this.updateMessageStatusByContent(message); + }); + } +} + +// 订阅消息列表变化 +// useEffect(() => { +// return service.messageStore.subscribe(state => { +// setMessages(state.messages); +// }); +// }, []); diff --git a/packages/pro-components/chat/chatbot/core/store/model.ts b/packages/pro-components/chat/chatbot/core/store/model.ts new file mode 100644 index 0000000000..249be548d3 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/store/model.ts @@ -0,0 +1,32 @@ +import type { ModelServiceState } from '../type'; +import ReactiveState from './reactiveState'; + +// 专注模型状态和运行时管理 +export class ModelStore extends ReactiveState { + initialize(initialState?: ModelServiceState) { + super.initialize({ + useSearch: false, + useThink: false, + model: '', + ...initialState, + }); + } + + setCurrentModel(modelName: string) { + this.setState((draft) => { + draft.model = modelName; + }); + } + + setUseThink(use: boolean) { + this.setState((draft) => { + draft.useThink = use; + }); + } + + setUseSearch(use: boolean) { + this.setState((draft) => { + draft.useSearch = use; + }); + } +} diff --git a/packages/pro-components/chat/chatbot/core/store/reactiveState.ts b/packages/pro-components/chat/chatbot/core/store/reactiveState.ts new file mode 100644 index 0000000000..c779c2289d --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/store/reactiveState.ts @@ -0,0 +1,146 @@ +import { enablePatches, produceWithPatches } from 'immer'; + +/** + * 状态订阅者回调函数类型 + * @template T 状态类型 + * @param state 只读的最新状态 + * @param changes 发生变更的路径数组(只读) + */ +export type Subscriber = (state: Readonly, changes: readonly string[]) => void; + +// 启用immer的patch支持,用于追踪状态变更路径 +enablePatches(); + +/** + * 响应式状态管理类,提供高效的状态管理和变更追踪功能 + * @template T 状态对象类型,必须为object类型 + */ +export default class ReactiveState { + private currentState: T; // 当前状态(始终为冻结对象) + + private subscribers = new Set<{ handler: Subscriber; paths?: string[] }>(); // 订阅者集合(包含路径过滤条件) + + private pendingChanges: string[] = []; // 待处理的变更路径(自动去重) + + private notificationScheduled = false; // 通知调度锁(防止重复调度) + + private pathSubscribers = new Map }>>(); // 增加订阅者分组缓存 + + /** + * 初始化响应式状态 + * @param initialState 初始状态(会自动冻结) + */ + public initialize(initialState: T) { + this.currentState = Object.freeze(initialState); + } + + /** + * 更新状态方法 + * @param updater 状态更新函数(使用immer的draft机制) + * @param paths 可选的手动指定变更路径(自动模式会从immer patches中提取) + */ + public setState(updater: (draft: T) => void, paths?: string[]): void { + // 使用produceWithPatches来获取变更路径,生成新状态和变更记录 + const [nextState, patches] = produceWithPatches(this.currentState, updater); + + // 处理变更路径:优先使用手动指定路径,否则从patches中提取 + const changes = + paths || patches.filter((p) => ['replace', 'add', 'remove'].includes(p.op)).map((p) => p.path.join('.')); + + if (changes.length > 0) { + this.pendingChanges.push(...changes); + this.currentState = Object.freeze(nextState) as T; + this.scheduleNotification(); + } + } + + /** + * 获取当前状态 + * @param cloned 是否返回克隆副本(默认false) + * @returns 当前状态的只读引用或克隆副本 + */ + public getState(cloned = false): Readonly { + return cloned ? structuredClone(this.currentState) : this.currentState; + } + + /** + * 订阅状态变更(支持路径过滤),订阅时维护路径索引 + * @param subscriber 订阅回调函数 + * @param paths 可选的要监听的属性路径数组 + * @returns 取消订阅的函数 + */ + public subscribe(subscriber: Subscriber, paths?: string[]): () => void { + const subscription = { handler: subscriber, paths }; + this.subscribers.add(subscription); + // 维护路径索引 + paths?.forEach((path) => { + if (!this.pathSubscribers.has(path)) { + this.pathSubscribers.set(path, new Set()); + } + this.pathSubscribers.get(path)?.add(subscription); + }); + + return () => { + this.subscribers.delete(subscription); + paths?.forEach((path) => { + this.pathSubscribers.get(path)?.delete(subscription); + }); + }; + } + + /** + * 调度通知(使用微任务批量处理) + */ + private scheduleNotification() { + if (this.notificationScheduled) return; + this.notificationScheduled = true; + + // 使用微任务进行批处理,确保在同一个事件循环内的多次更新只会触发一次通知 + queueMicrotask(() => { + // 去重变更路径并重置待处理队列 + const changedPaths = [...new Set(this.pendingChanges)]; + this.pendingChanges = []; + this.notificationScheduled = false; + + // 冻结状态和路径数组,防止订阅者意外修改 + const frozenState = Object.freeze(this.currentState); + const frozenPaths = Object.freeze(changedPaths); + + // 安全通知所有订阅者 + this.subscribers.forEach(({ handler, paths }) => { + try { + // 如果没有设置监听路径,或变更路径中有匹配项,则触发回调 + if ( + !paths || + frozenPaths.some((p) => + paths.some((target) => { + const targetParts = target.split('.'); + const pathParts = p.split('.'); + return targetParts.every((part, i) => pathParts[i] === part); + }), + ) + ) { + handler(frozenState, frozenPaths); + } + } catch (error) { + console.error('Subscriber error:', error); + } + }); + }); + } + + /** + * 调试方法(开发时使用) + * @param label 调试标签(默认'State') + * @returns 当前实例(支持链式调用) + */ + public debug(label = 'State'): this { + this.subscribe((state, paths) => { + console.groupCollapsed(`%c${label} Update`, 'color: #4CAF50; font-weight: bold;'); + console.log('Changed Paths:', paths); + console.log('New State:', state); + console.groupEnd(); + }); + return this; + } +} diff --git a/packages/pro-components/chat/chatbot/core/type.ts b/packages/pro-components/chat/chatbot/core/type.ts new file mode 100644 index 0000000000..5de1770a58 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/type.ts @@ -0,0 +1,341 @@ +export type ChatMessageRole = 'user' | 'assistant' | 'system'; +export type ChatMessageStatus = 'pending' | 'streaming' | 'complete' | 'stop' | 'error'; +export type ChatStatus = 'idle' | ChatMessageStatus; +export type ChatContentType = + | 'text' + | 'markdown' + | 'search' + | 'attachment' + | 'thinking' + | 'image' + | 'audio' + | 'video' + | 'suggestion'; +export type AttachmentType = 'image' | 'video' | 'audio' | 'pdf' | 'doc' | 'ppt' | 'txt'; + +// 基础类型 +export interface ChatBaseContent { + type: T; + data: TData; + status?: ChatMessageStatus | ((currentStatus: ChatMessageStatus | undefined) => ChatMessageStatus); + id?: string; +} + +// 内容类型 +export type TextContent = ChatBaseContent<'text', string>; + +export type MarkdownContent = ChatBaseContent<'markdown', string>; + +export type ImageContent = ChatBaseContent< + 'image', + { + name?: string; + url?: string; + width?: number; + height?: number; + } +>; + +// 搜索 +// 公共引用结构 +export type ReferenceItem = { + title: string; + icon?: string; + type?: string; + url?: string; + content?: string; + site?: string; + date?: string; +}; +export type SearchContent = ChatBaseContent< + 'search', + { + title?: string; + references?: ReferenceItem[]; + } +>; + +export type SuggestionItem = { + title: string; + prompt?: string; +}; +export type SuggestionContent = ChatBaseContent<'suggestion', SuggestionItem[]>; + +// 附件消息 +export type AttachmentItem = { + fileType: AttachmentType; + size?: number; + name?: string; + url?: string; + isReference?: boolean; // 是否是引用 + width?: number; + height?: number; + extension?: string; // 自定义文件后缀,默认按照name文件名后缀识别 + metadata?: Record; +}; +export type AttachmentContent = ChatBaseContent<'attachment', AttachmentItem[]>; + +// 思考过程 +export type ThinkingContent = ChatBaseContent< + 'thinking', + { + text?: string; + title?: string; + } +>; + +// 消息主体 +// 基础消息结构 + +export interface ChatBaseMessage { + id: string; + status?: ChatMessageStatus; + datetime?: string; + ext?: any; +} + +// 类型扩展机制 +declare global { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface AIContentTypeOverrides {} +} + +type AIContentTypeMap = { + text: TextContent; + markdown: MarkdownContent; + thinking: ThinkingContent; + image: ImageContent; + search: SearchContent; + suggestion: SuggestionContent; +} & AIContentTypeOverrides; + +// 自动生成联合类型 +// export type AIMessageContent = AIContentTypeMap[keyof AIContentTypeMap]; +// export type AIMessageContent = { +// [K in keyof AIContentTypeMap]: AIContentTypeMap[K]; +// }[keyof AIContentTypeMap]; + +export type AIContentType = keyof AIContentTypeMap; +export type AIMessageContent = AIContentTypeMap[AIContentType]; +export type UserMessageContent = TextContent | AttachmentContent; + +export interface UserMessage extends ChatBaseMessage { + role: 'user'; + content: UserMessageContent[]; +} + +export type ChatComment = 'good' | 'bad' | ''; + +export interface AIMessage extends ChatBaseMessage { + role: 'assistant'; + content?: AIMessageContent[]; + /** 点赞点踩 */ + comment?: ChatComment; +} + +export interface SystemMessage extends ChatBaseMessage { + role: 'system'; + content: TextContent[]; +} + +export type ChatMessagesData = UserMessage | AIMessage | SystemMessage; + +// 回答消息体配置 +export type SSEChunkData = { + event?: string; + data: any; +}; + +export interface ChatRequestParams extends RequestInit { + prompt: string; + messageID?: string; + attachments?: AttachmentContent['data']; +} + +// 基础配置类型 +export type AIContentChunkUpdate = AIMessageContent & { + // 将新内容块和入策略,merge表示和入到同类型内容中,append表示作为新的内容块,默认是merge + strategy?: 'merge' | 'append'; +}; + +// ============= TDesign 原生架构 ============= +// 保持现有的TDesign类型不变,用于DefaultEngine + +// 网络请求配置(TDesign原生) +export interface ChatNetworkConfig { + /** 请求端点 */ + endpoint?: string; + /** 是否启用流式传输 */ + stream?: boolean; + /** 重试间隔(毫秒) */ + retryInterval?: number; + /** 最大重试次数 */ + maxRetries?: number; + /** 请求超时时间(毫秒) */ + timeout?: number; // 添加timeout属性 + /** 协议类型 */ + protocol?: 'default' | 'agui'; +} + +// 添加EnhancedSSEClient接口定义 +export interface IEnhancedSSEClient { + close(): void; + getStats(): any; + // ... 其他方法 ... +} + +// TDesign 默认引擎的回调配置 +export interface DefaultEngineCallbacks { + /** 请求发送前配置 */ + onRequest?: (params: ChatRequestParams) => RequestInit | Promise; + onStart?: (chunk: string) => void; + /** 接收到消息数据块 - 用于解析和处理聊天内容 */ + onMessage?: (chunk: SSEChunkData, message?: ChatMessagesData) => AIContentChunkUpdate | AIMessageContent[] | null; + onComplete?: ( + isAborted: boolean, + params?: RequestInit, + result?: any, + ) => AIContentChunkUpdate | AIMessageContent[] | null; + onAbort?: () => Promise; + /** 错误处理 */ + onError?: (err: Error | Response) => void; +} + +// 默认引擎完整的服务配置 +export interface ChatServiceConfig extends ChatNetworkConfig, DefaultEngineCallbacks {} + +// 联合类型支持静态配置和动态生成 +export type ChatServiceConfigSetter = ChatServiceConfig | ((params?: any) => ChatServiceConfig); + +// ============= AG-UI 适配架构 ============= +// AG-UI 服务配置(完全独立) +export interface AGUIServiceConfig { + /** AG-UI 服务端点 */ + url: string; + /** Agent ID */ + agentId?: string; + /** 请求头 */ + headers?: Record; + /** 初始状态 */ + initialState?: Record; + /** 线程ID */ + threadId?: string; + /** 工具定义 */ + tools?: any[]; + /** 上下文信息 */ + context?: any[]; + /** 调试模式 */ + debug?: boolean; +} + +// AG-UI 事件回调(基于AG-UI原生事件) +export interface AGUIEventCallbacks { + /** 运行开始事件 */ + onRunStarted?: (threadId: string, runId: string) => void; + /** 运行完成事件 */ + onRunFinished?: (threadId: string, runId: string, result?: any) => void; + /** 运行错误事件 */ + onRunError?: (error: string, code?: string) => void; + + /** 文本消息开始 */ + onTextMessageStart?: (messageId: string) => void; + /** 文本消息内容 */ + onTextMessageContent?: (messageId: string, delta: string) => void; + /** 文本消息结束 */ + onTextMessageEnd?: (messageId: string) => void; + + /** 工具调用开始 */ + onToolCallStart?: (toolCallId: string, toolName: string, parentMessageId?: string) => void; + /** 工具调用参数 */ + onToolCallArgs?: (toolCallId: string, delta: string) => void; + /** 工具调用结束 */ + onToolCallEnd?: (toolCallId: string) => void; + /** 工具调用结果 */ + onToolCallResult?: (messageId: string, toolCallId: string, content: string) => void; + + /** 状态快照 */ + onStateSnapshot?: (snapshot: any) => void; + /** 状态增量 */ + onStateDelta?: (delta: any[]) => void; + /** 消息快照 */ + onMessagesSnapshot?: (messages: any[]) => void; + + /** 自定义事件 */ + onCustomEvent?: (name: string, value: any) => void; + /** 原始事件 */ + onRawEvent?: (event: any, source?: string) => void; +} + +// ============= 引擎接口统一 ============= +// 引擎模式 +export type EngineMode = 'default' | 'agui'; + +// 统一的引擎接口 +export interface IChatEngine { + /** 引擎模式 */ + readonly mode: EngineMode; + + /** 初始化引擎 - 不同引擎使用不同的配置类型 */ + init(config?: any, messages?: ChatMessagesData[]): void; + + /** 发送用户消息 */ + sendUserMessage(params: ChatRequestParams): Promise; + + /** 重新生成AI回复 */ + regenerateAIMessage(keepVersion?: boolean): Promise; + + /** 中止聊天 */ + abortChat(): Promise; + + /** 设置消息 */ + setMessages(messages: ChatMessagesData[], mode?: ChatMessageSetterMode): void; + + /** 清空消息 */ + clearMessages(): void; + + /** 注册合并策略 */ + registerMergeStrategy(type: T['type'], handler: (chunk: T, existing?: T) => T): void; + + // 属性访问 + get messages(): ChatMessagesData[]; + get status(): ChatStatus; + get messageStore(): any; // 抽象化,不同引擎可能有不同的store + + // 销毁 + destroy(): void; +} + +// 消息相关状态 +export interface ChatMessageStore { + messageIds: string[]; + messages: ChatMessagesData[]; +} + +// 模型服务相关状态 +export interface ModelParams { + model?: string; + useThink?: boolean; + useSearch?: boolean; +} + +export interface ModelServiceState extends ModelParams { + config: ChatServiceConfig; +} + +// 聚合根状态 +export interface ChatState { + message: ChatMessageStore; + model: ModelServiceState; +} + +export type ChatMessageSetterMode = 'replace' | 'prepend' | 'append'; + +export type AIContentHandler> = (chunk: T, existing?: T) => T; + +export interface ContentTypeDefinition { + type: T; + handler?: AIContentHandler>; + renderer?: ContentRenderer>; +} + +export type ContentRenderer> = (content: T) => unknown; diff --git a/packages/pro-components/chat/chatbot/core/utils/eventEmitter.ts b/packages/pro-components/chat/chatbot/core/utils/eventEmitter.ts new file mode 100644 index 0000000000..c90d9fffc5 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/utils/eventEmitter.ts @@ -0,0 +1,46 @@ +/** + * 简单的 EventEmitter 实现,用于浏览器环境 + */ +export default class SimpleEventEmitter { + private events: Map void>> = new Map(); + + on(event: string, listener: (...args: any[]) => void): void { + if (!this.events.has(event)) { + this.events.set(event, []); + } + this.events.get(event)!.push(listener); + } + + off(event: string, listener: (...args: any[]) => void): void { + const listeners = this.events.get(event); + if (listeners) { + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + } + } + + emit(event: string, ...args: any[]): boolean { + const listeners = this.events.get(event); + if (listeners && listeners.length > 0) { + listeners.forEach((listener) => { + try { + listener(...args); + } catch (error) { + console.error('EventEmitter listener error:', error); + } + }); + return true; + } + return false; + } + + removeAllListeners(event?: string): void { + if (event) { + this.events.delete(event); + } else { + this.events.clear(); + } + } +} diff --git a/packages/pro-components/chat/chatbot/core/utils/index.ts b/packages/pro-components/chat/chatbot/core/utils/index.ts new file mode 100644 index 0000000000..5003ff9287 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/utils/index.ts @@ -0,0 +1,89 @@ +import { + AIMessageContent, + AttachmentContent, + ChatMessagesData, + ImageContent, + MarkdownContent, + SearchContent, + SuggestionContent, + TextContent, + ThinkingContent, + UserMessageContent, +} from 'tdesign-web-components'; + +export function findTargetElement(event: MouseEvent, selector: string | string[]): HTMLElement | null { + // 统一处理选择器输入格式(支持字符串或数组) + const selectors = Array.isArray(selector) ? selector : selector.split(',').map((s) => s.trim()); + + // 获取事件穿透路径(包含Shadow DOM内部元素) + const eventPath = event.composedPath(); + + // 遍历路径查找目标元素 + for (const el of eventPath) { + // 类型安全判断 + 多选择器匹配 + if (el instanceof HTMLElement) { + // 检查是否匹配任意一个选择器 + if (selectors.some((sel) => el.matches?.(sel))) { + return el; // 找到即返回 + } + } + } + + return null; // 未找到返回null +} + +// 类型守卫函数 +export function isUserMessage(message: ChatMessagesData) { + return message.role === 'user' && 'content' in message; +} + +export function isAIMessage(message: ChatMessagesData) { + return message.role === 'assistant'; +} + +export function isThinkingContent(content: AIMessageContent): content is ThinkingContent { + return content.type === 'thinking'; +} + +export function isTextContent(content: AIMessageContent): content is TextContent { + return content.type === 'text'; +} + +export function isMarkdownContent(content: AIMessageContent): content is MarkdownContent { + return content.type === 'markdown'; +} + +export function isImageContent(content: AIMessageContent): content is ImageContent { + return content.type === 'image'; +} + +export function isSearchContent(content: AIMessageContent): content is SearchContent { + return content.type === 'search'; +} + +export function isSuggestionContent(content: AIMessageContent): content is SuggestionContent { + return content.type === 'suggestion'; +} + +export function isAttachmentContent(content: UserMessageContent): content is AttachmentContent { + return content.type === 'attachment'; +} + +/** 提取消息复制内容 */ +export function getMessageContentForCopy(message: ChatMessagesData): string { + if (!isAIMessage(message)) { + return ''; + } + return message.content.reduce((pre, item) => { + let append = ''; + if (isTextContent(item) || isMarkdownContent(item)) { + append = item.data; + } else if (isThinkingContent(item)) { + append = item.data.text; + } + if (!pre) { + return append; + } + return `${pre}\n${append}`; + }, ''); +} diff --git a/packages/pro-components/chat/chatbot/core/utils/logger.ts b/packages/pro-components/chat/chatbot/core/utils/logger.ts new file mode 100644 index 0000000000..41ae88c597 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/utils/logger.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-await-in-loop, max-classes-per-file */ +// 日志接口 +export interface Logger { + debug(message: string, ...args: any[]): void; + info(message: string, ...args: any[]): void; + warn(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} + +// 默认日志实现 +export class ConsoleLogger implements Logger { + private enableDebug: boolean; + + constructor(enableDebug = false) { + this.enableDebug = enableDebug; + } + + debug(message: string, ...args: any[]): void { + if (this.enableDebug) { + console.debug(`[SSE Debug] ${message}`, ...args); + } + } + + info(message: string, ...args: any[]): void { + console.info(`[SSE Info] ${message}`, ...args); + } + + warn(message: string, ...args: any[]): void { + console.warn(`[SSE Warn] ${message}`, ...args); + } + + error(message: string, ...args: any[]): void { + console.error(`[SSE Error] ${message}`, ...args); + } +} + +export class LoggerManager { + private static instance: Logger; + + private static customLogger: Logger | null = null; + + /** + * 获取日志实例(单例模式) + */ + static getLogger(): Logger { + if (this.customLogger) { + return this.customLogger; + } + + if (!this.instance) { + this.instance = new ConsoleLogger(); + } + return this.instance; + } + + /** + * 设置自定义日志实例 + */ + static setLogger(logger: Logger): void { + this.customLogger = logger; + } + + /** + * 重置为默认日志 + */ + static resetToDefault(): void { + this.customLogger = null; + } +} diff --git a/packages/pro-components/chat/chatbot/useChat.ts b/packages/pro-components/chat/chatbot/useChat.ts index 4e4f3eecb5..301b15d217 100644 --- a/packages/pro-components/chat/chatbot/useChat.ts +++ b/packages/pro-components/chat/chatbot/useChat.ts @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; -import type { ChatMessagesData, ChatStatus } from 'tdesign-web-components/lib/chatbot/core/type'; import { TdChatProps } from 'tdesign-web-components'; -import ChatEngine from 'tdesign-web-components/lib/chatbot/core'; +import ChatEngine from './core'; +import type { ChatMessagesData, ChatStatus } from './core/type'; // @ts-ignore export type IUseChat = Pick; diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 495dd8e88c..97f39df2a7 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -51,6 +51,7 @@ "dependencies": { "@babel/runtime": "~7.26.7", "tdesign-web-components": "1.1.5", + "zod": "^3.23.8", "classnames": "~2.5.1", "lodash-es": "^4.17.21" }, @@ -61,4 +62,4 @@ "tvision-charts-react": "^3.3.12", "express": "^4.17.3" } -} +} \ No newline at end of file From 44d633773f7205f3b4d53c38e4ab25e9b6465a6c Mon Sep 17 00:00:00 2001 From: lincao Date: Mon, 21 Jul 2025 08:54:09 +0800 Subject: [PATCH 116/228] feat(chatbot): add agui basic --- .../pro-components/chat/chatbot/chatbot.md | 151 ++++++++++-------- 1 file changed, 80 insertions(+), 71 deletions(-) diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index 92305c1142..37d65916bd 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -8,126 +8,135 @@ spline: navigation ## 基本用法 ### 标准化集成 -组件内置状态管理,SSE解析,自动处理消息内容渲染与交互逻辑,可开箱即用快速集成实现标准聊天界面。本示例演示了如何快速创建一个具备以下功能的智能对话组件: - - 初始化预设消息 - - 预设消息内容渲染支持(markdown、搜索、思考、建议等) - - 与服务端的SSE(Server-Sent Events)通信,支持流式消息响应 - - 自定义流式内容结构解析 - - 自定义请求参数处理 - - 常用消息操作处理及回调(复制、重试、点赞/点踩) - - 支持手动触发填入prompt, 重新生成,发送消息等 -{{ basic }} +组件内置状态管理,SSE 解析,自动处理消息内容渲染与交互逻辑,可开箱即用快速集成实现标准聊天界面。本示例演示了如何快速创建一个具备以下功能的智能对话组件: + +- 初始化预设消息 +- 预设消息内容渲染支持(markdown、搜索、思考、建议等) +- 与服务端的 SSE(Server-Sent Events)通信,支持流式消息响应 +- 自定义流式内容结构解析 +- 自定义请求参数处理 +- 常用消息操作处理及回调(复制、重试、点赞/点踩) +- 支持手动触发填入 prompt, 重新生成,发送消息等 +{{ basic }} ### 组合式用法 -可以通过 `useChat` Hook提供的对话引擎实例及状态控制方法,同时自行组合拼装`ChatList`,`ChatMessage`, `ChatSender`等组件集成聊天界面,适合需要深度定制组件结构和消息处理流程的场景 + +可以通过 `useChat` Hook 提供的对话引擎实例及状态控制方法,同时自行组合拼装`ChatList`,`ChatMessage`, `ChatSender`等组件集成聊天界面,适合需要深度定制组件结构和消息处理流程的场景 {{ hookComponent }} ## 自定义 + 如果组件内置的消息渲染方案不能满足需求,还可以通过自定义**消息结构解析逻辑**和**消息内容渲染组件**来实现更多渲染需求。以下示例给出了一个自定义实现图表渲染的示例,实现自定义渲染需要完成**四步**,概括起来就是:**扩展类型,准备组件,解析数据,植入插槽**: -- 1、扩展自定义消息体type类型 -- 2、实现自定义渲染的组件,示例中使用了tvision-charts-react实现图表渲染 + +- 1、扩展自定义消息体 type 类型 +- 2、实现自定义渲染的组件,示例中使用了 tvision-charts-react 实现图表渲染 - 3、流式数据增量更新回调`onMessage`中可以对返回数据进行标准化解构,返回渲染组件所需的数据结构,同时可以通过返回`strategy`来决定**同类新增内容块**的追加策略(merge/append),如果需要更灵活影响到数据整合可以返回完整消息数组`AIMessageContent[]`,或者注册合并策略方法(参考下方‘任务规划’示例) -- 4、在render函数中遍历消息内容数组,植入自定义消息体渲染插槽,需保证slot名在list中的唯一性 +- 4、在 render 函数中遍历消息内容数组,植入自定义消息体渲染插槽,需保证 slot 名在 list 中的唯一性 如果组件内置的几种操作 `TdChatMessageActionName` 不能满足需求,示例中同时给出了**自定义消息操作区**的方法,可以自行实现更多操作。 {{ custom }} - ## 场景化示例 + 以下再通过几个常见的业务场景,展示下如何使用 `Chatbot` 组件 ### 代码助手 -通过使用tdesign开发登录框组件的案例,演示了使用Chatbot搭建简单的代码助手场景,该示例你可以了解到如何按需开启**markdown渲染代码块**,如何**自定义实现代码预览** + +通过使用 tdesign 开发登录框组件的案例,演示了使用 Chatbot 搭建简单的代码助手场景,该示例你可以了解到如何按需开启**markdown 渲染代码块**,如何**自定义实现代码预览** {{ code }} ### 文案助手 -以下案例演示了使用Chatbot搭建简单的文案写作助手应用,通过该示例你可以了解到如何**发送附件**,同时演示了**附件类型的内容渲染** + +以下案例演示了使用 Chatbot 搭建简单的文案写作助手应用,通过该示例你可以了解到如何**发送附件**,同时演示了**附件类型的内容渲染** {{ docs }} ### 图像生成 -以下案例演示了使用Chatbot搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** + +以下案例演示了使用 Chatbot 搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** {{ image }} ### 任务规划 -以下案例模拟了使用Chatbot搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,**自定义消息插槽名规则**,同时演示了**自定义任务流程渲染** + +以下案例模拟了使用 Chatbot 搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,**自定义消息插槽名规则**,同时演示了**自定义任务流程渲染** {{ agent }} +### AGUI 协议支持 + +{{ agui }} ## API + ### Chatbot Props -名称 | 类型 | 默认值 | 说明 | 必传 --- | -- | -- | -- | -- -defaultMessages | Array | - | 初始消息数据列表。TS类型:`ChatMessagesData[]`。[详细类型定义](/react-aigc/components/chat-message?tab=api) | N -messageProps | Object/Function | - | 消息项配置。按角色聚合了消息项的配置透传`ChatMessage`组件,TS类型:`TdChatMessageConfig \| ((msg: ChatMessagesData) => Omit)` ,[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/blob/develop/src/chatbot/type.ts#L151) | N -listProps | Object | - | 消息列表配置。TS类型:`TdChatListProps`。 | N -senderProps | Object | - | 发送框配置,透传`ChatSender`组件。TS类型:`TdChatSenderProps`。[类型定义](./chat-sender?tab=api) | N -chatServiceConfig | Object | - | 聊天服务配置,见下方详细说明,TS类型:`ChatServiceConfig` | N -onMessageChange | Function | - | 消息列表数据变化回调,TS类型:`(e: CustomEvent) => void` | N -onChatReady | Function | - | 内部消息引擎初始化完成回调,TS类型:`(e: CustomEvent) => void` | N -onChatSent | Function | - | 发送消息回调,TS类型:`(e: CustomEvent) => void` | N +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| ----------------- | --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | +| defaultMessages | Array | - | 初始消息数据列表。TS 类型:`ChatMessagesData[]`。[详细类型定义](/react-aigc/components/chat-message?tab=api) | N | +| messageProps | Object/Function | - | 消息项配置。按角色聚合了消息项的配置透传`ChatMessage`组件,TS 类型:`TdChatMessageConfig \| ((msg: ChatMessagesData) => Omit)` ,[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/blob/develop/src/chatbot/type.ts#L151) | N | +| listProps | Object | - | 消息列表配置。TS 类型:`TdChatListProps`。 | N | +| senderProps | Object | - | 发送框配置,透传`ChatSender`组件。TS 类型:`TdChatSenderProps`。[类型定义](./chat-sender?tab=api) | N | +| chatServiceConfig | Object | - | 聊天服务配置,见下方详细说明,TS 类型:`ChatServiceConfig` | N | +| onMessageChange | Function | - | 消息列表数据变化回调,TS 类型:`(e: CustomEvent) => void` | N | +| onChatReady | Function | - | 内部消息引擎初始化完成回调,TS 类型:`(e: CustomEvent) => void` | N | +| onChatSent | Function | - | 发送消息回调,TS 类型:`(e: CustomEvent) => void` | N | ### TdChatListProps 消息列表配置 -名称 | 类型 | 默认值 | 说明 | 必传 --- | -- | -- | -- | -- -autoScroll | Boolean | true | 高度变化时列表是否自动滚动到底部 | N -defaultScrollTo | String | bottom | 默认初始时滚动定位。可选项:top/bottom/bottom | N -onScroll | Function | - | 滚动事件回调 | N - - +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| --------------- | -------- | ------ | --------------------------------------------- | ---- | +| autoScroll | Boolean | true | 高度变化时列表是否自动滚动到底部 | N | +| defaultScrollTo | String | bottom | 默认初始时滚动定位。可选项:top/bottom/bottom | N | +| onScroll | Function | - | 滚动事件回调 | N | ### ChatServiceConfig 类型说明 -聊天服务核心配置类型,主要作用包括基础通信配置,请求流程控制及全生命周期管理(初始化→传输→完成/中止),流式数据的分块处理策略,状态通知回调等。 +聊天服务核心配置类型,主要作用包括基础通信配置,请求流程控制及全生命周期管理(初始化 → 传输 → 完成/中止),流式数据的分块处理策略,状态通知回调等。 -名称 | 类型 | 默认值 | 说明 | 必传 --- | -- | -- | -- | -- -endpoint | String | - | 聊天服务请求地址url | N -stream | Boolean | true | 是否使用流式传输 | N -onRequest | Function | - | 请求前的回调,可修改请求参数。TS类型:`(params: ChatRequestParams) => RequestInit` | N -onMessage | Function | - | 处理流式消息的回调。TS类型:`(chunk: SSEChunkData) => AIMessageContent / null` | N -onComplete | Function | - | 请求结束时的回调。TS类型:`(isAborted: boolean, params: RequestInit, result?: any) => void` | N -onAbort | Function | - | 中止请求时的回调。TS类型:`() => Promise` | N -onError | Function | - | 错误处理回调。TS类型:`(err: Error \| Response) => void` | N +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| ---------- | -------- | ------ | -------------------------------------------------------------------------------------------- | ---- | +| endpoint | String | - | 聊天服务请求地址 url | N | +| stream | Boolean | true | 是否使用流式传输 | N | +| onRequest | Function | - | 请求前的回调,可修改请求参数。TS 类型:`(params: ChatRequestParams) => RequestInit` | N | +| onMessage | Function | - | 处理流式消息的回调。TS 类型:`(chunk: SSEChunkData) => AIMessageContent / null` | N | +| onComplete | Function | - | 请求结束时的回调。TS 类型:`(isAborted: boolean, params: RequestInit, result?: any) => void` | N | +| onAbort | Function | - | 中止请求时的回调。TS 类型:`() => Promise` | N | +| onError | Function | - | 错误处理回调。TS 类型:`(err: Error \| Response) => void` | N | ### Chatbot 实例方法 -名称 | 类型 | 描述 --- | -- | -- -setMessages | (messages: ChatMessagesData[], mode?: 'replace' \| 'prepend' \| 'append') => void | 批量设置消息 -sendUserMessage | (params: ChatRequestParams) => Promise | 发送用户消息,处理请求参数并触发消息流 -sendSystemMessage | (msg: string) => void | 发送系统级通知消息,用于展示系统提示/警告 -abortChat | () => Promise | 中止当前进行中的聊天请求,清理网络连接 -addPrompt | (prompt: string) => void | 将预设提示语添加到输入框,辅助用户快速输入 -selectFile | () => void | 触发文件选择对话框,用于附件上传功能 -regenerate | (keepVersion?: boolean) => Promise | 重新生成最后一条消息,可选保留历史版本 -registerMergeStrategy | (type: T['type'], handler: (chunk: T, existing?: T) => T) => void | 注册自定义消息合并策略,用于处理流式数据更新 -scrollList | ({ to: 'bottom' \| 'top', behavior: 'auto' \| 'smooth' }) => void | 受控滚动到指定位置 -isChatEngineReady | boolean | ChatEngine是否就绪 -chatMessageValue | ChatMessagesData[] | 获取当前消息列表的只读副本 -chatStatus | ChatStatus | 获取当前聊天状态(空闲/进行中/错误等) -senderLoading | boolean | 当前输入框按钮是否在'输出中' - +| 名称 | 类型 | 描述 | +| --------------------- | --------------------------------------------------------------------------------- | -------------------------------------------- | +| setMessages | (messages: ChatMessagesData[], mode?: 'replace' \| 'prepend' \| 'append') => void | 批量设置消息 | +| sendUserMessage | (params: ChatRequestParams) => Promise | 发送用户消息,处理请求参数并触发消息流 | +| sendSystemMessage | (msg: string) => void | 发送系统级通知消息,用于展示系统提示/警告 | +| abortChat | () => Promise | 中止当前进行中的聊天请求,清理网络连接 | +| addPrompt | (prompt: string) => void | 将预设提示语添加到输入框,辅助用户快速输入 | +| selectFile | () => void | 触发文件选择对话框,用于附件上传功能 | +| regenerate | (keepVersion?: boolean) => Promise | 重新生成最后一条消息,可选保留历史版本 | +| registerMergeStrategy | (type: T['type'], handler: (chunk: T, existing?: T) => T) => void | 注册自定义消息合并策略,用于处理流式数据更新 | +| scrollList | ({ to: 'bottom' \| 'top', behavior: 'auto' \| 'smooth' }) => void | 受控滚动到指定位置 | +| isChatEngineReady | boolean | ChatEngine 是否就绪 | +| chatMessageValue | ChatMessagesData[] | 获取当前消息列表的只读副本 | +| chatStatus | ChatStatus | 获取当前聊天状态(空闲/进行中/错误等) | +| senderLoading | boolean | 当前输入框按钮是否在'输出中' | ### useChat Hook -useChat 是聊天组件核心逻辑Hook,用于管理聊天状态与生命周期:初始化聊天引擎、同步消息数据、订阅状态变更,并自动处理组件卸载时的资源清理,对外暴露聊天引擎实例/消息列表/状态等核心参数。 +useChat 是聊天组件核心逻辑 Hook,用于管理聊天状态与生命周期:初始化聊天引擎、同步消息数据、订阅状态变更,并自动处理组件卸载时的资源清理,对外暴露聊天引擎实例/消息列表/状态等核心参数。 - **请求参数说明** -参数名 | 类型 | 说明 --- | -- | -- -defaultMessages | ChatMessagesData[] | 初始化消息列表,用于设置聊天记录的初始值 -chatServiceConfig | ChatServiceConfigSetter | 聊天服务配置,支持静态配置或动态生成配置的函数,用于设置API端点/重试策略等参数 +| 参数名 | 类型 | 说明 | +| ----------------- | ----------------------- | -------------------------------------------------------------------------------- | +| defaultMessages | ChatMessagesData[] | 初始化消息列表,用于设置聊天记录的初始值 | +| chatServiceConfig | ChatServiceConfigSetter | 聊天服务配置,支持静态配置或动态生成配置的函数,用于设置 API 端点/重试策略等参数 | - **返回值说明** -返回值 | 类型 | 说明 --- | -- | -- -chatEngine | IChatEngine | 聊天引擎实例,提供核心操作方法,同上方 `Chatbot 实例方法` -messages | ChatMessagesData[] | 当前聊天消息列表所有数据 -status | ChatStatus | 当前聊天状态 +| 返回值 | 类型 | 说明 | +| ---------- | ------------------ | --------------------------------------------------------- | +| chatEngine | IChatEngine | 聊天引擎实例,提供核心操作方法,同上方 `Chatbot 实例方法` | +| messages | ChatMessagesData[] | 当前聊天消息列表所有数据 | +| status | ChatStatus | 当前聊天状态 | From fc6abb2c77fe605372eecd243f7fb84388c2a298 Mon Sep 17 00:00:00 2001 From: lincao Date: Mon, 21 Jul 2025 08:57:15 +0800 Subject: [PATCH 117/228] feat(chatbot): add agui demo n --- packages/pro-components/chat/chatbot/_example/agui.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/agui.tsx b/packages/pro-components/chat/chatbot/_example/agui.tsx index b81f8eab8a..5f3a5085f3 100644 --- a/packages/pro-components/chat/chatbot/_example/agui.tsx +++ b/packages/pro-components/chat/chatbot/_example/agui.tsx @@ -3,7 +3,6 @@ import { type TdChatMessageConfig, type ChatRequestParams, type ChatMessagesData, - type ChatBaseContent, ChatList, ChatSender, ChatMessage, @@ -19,7 +18,7 @@ import mockData from './mock/data'; export default function ComponentsBuild() { const listRef = useRef(null); const inputRef = useRef(null); - const [inputValue, setInputValue] = useState('南极的自动提款机叫什么名字'); + const [inputValue, setInputValue] = useState('AG-UI协议的作用是什么'); const { chatEngine, messages, status } = useChat({ defaultMessages: mockData.normal, // 聊天服务配置 From 41c5466f2be8b91d095552470d677d314b3f4504 Mon Sep 17 00:00:00 2001 From: lincao Date: Mon, 21 Jul 2025 09:29:41 +0800 Subject: [PATCH 118/228] feat(chatbot): add suggestion --- .../chat/chatbot/_example/agui.tsx | 2 +- .../core/adapters/agui/agui-event-mapper.ts | 37 ++++++++++++++- .../pro-components/chat/chatbot/core/index.ts | 31 +++++++++++++ .../chat/chatbot/core/processor/index.ts | 28 ++++++++++++ .../chat/chatbot/core/processor/registry.ts | 45 +++++++++++++++++++ 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 packages/pro-components/chat/chatbot/core/processor/registry.ts diff --git a/packages/pro-components/chat/chatbot/_example/agui.tsx b/packages/pro-components/chat/chatbot/_example/agui.tsx index 5f3a5085f3..c17657b7a8 100644 --- a/packages/pro-components/chat/chatbot/_example/agui.tsx +++ b/packages/pro-components/chat/chatbot/_example/agui.tsx @@ -20,7 +20,7 @@ export default function ComponentsBuild() { const inputRef = useRef(null); const [inputValue, setInputValue] = useState('AG-UI协议的作用是什么'); const { chatEngine, messages, status } = useChat({ - defaultMessages: mockData.normal, + defaultMessages: [], // 聊天服务配置 chatServiceConfig: { // 对话服务地址 diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts index 2a888df705..89cb8bda5a 100644 --- a/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts @@ -46,7 +46,6 @@ export class AGUIEventMapper { case EventType.THINKING_TEXT_MESSAGE_CONTENT: return { type: 'thinking', data: { text: event.delta }, status: 'streaming', strategy: 'merge' }; case EventType.THINKING_END: - console.log('=====think end', event); return { type: 'thinking', data: { title: event.title || '思考结束' }, status: 'complete' }; case EventType.TOOL_CALL_START: @@ -68,7 +67,6 @@ export class AGUIEventMapper { return null; case EventType.TOOL_CALL_CHUNK: case EventType.TOOL_CALL_RESULT: - console.log('====parsed', event); if (event.toolCallName === 'search') { let parsed = { title: '搜索中', @@ -157,3 +155,38 @@ export class AGUIEventMapper { } export default AGUIEventMapper; + +// import { strategyRegistry } from '../../strategy/strategy-registry'; + +// export class AGUIEventMapper { +// // 注册AGUI协议相关策略 +// static registerDefaultStrategies() { +// // 工具调用策略 +// strategyRegistry.register('tool-call', (chunk, existing) => ({ +// ...(existing || {}), +// ...chunk, +// data: { +// ...(existing?.data || {}), +// ...chunk.data, +// arguments: (existing?.data?.arguments || '') + (chunk.data?.arguments || '') +// } +// })); + +// // 步骤状态策略 +// strategyRegistry.register('step', (chunk, existing) => { +// const updated = { ...existing, ...chunk }; +// if (chunk.data?.tasks) { +// updated.data.tasks = [ +// ...(existing?.data?.tasks || []), +// ...chunk.data.tasks +// ]; +// } +// return updated; +// }); +// } + +// // ... 原有 mapEvent 方法保持不变 ... +// } + +// // 初始化时注册默认策略 +// AGUIEventMapper.registerDefaultStrategies(); diff --git a/packages/pro-components/chat/chatbot/core/index.ts b/packages/pro-components/chat/chatbot/core/index.ts index 04e88ee4e3..ba39e536ff 100644 --- a/packages/pro-components/chat/chatbot/core/index.ts +++ b/packages/pro-components/chat/chatbot/core/index.ts @@ -307,3 +307,34 @@ export default class ChatEngine implements IChatEngine { } export * from './utils'; + +// ... existing code ... +// export default class ChatEngine implements IChatEngine { +// // 移除原有的 processor 实例 + +// public registerMergeStrategy( +// type: T['type'], +// handler: (chunk: T, existing?: T) => T +// ) { +// // 直接注册到策略中心 +// strategyRegistry.register(type, handler); +// } + +// // 修改 processContentUpdate 方法 +// private processContentUpdate(messageId: string, rawChunk: AIContentChunkUpdate) { +// // ... 原有逻辑 ... +// // 替换为: +// const strategy = strategyRegistry.get(rawChunk.type); +// const processed = strategy +// ? strategy(rawChunk, lastContent) +// : this.defaultMerge(rawChunk, lastContent); +// // ... +// } +// } + +// 使用:注册自定义策略 +// chatEngine.registerMergeStrategy('agent', (chunk, existing) => { +// const updated = { ...existing, ...chunk }; +// // 自定义合并逻辑... +// return updated; +// }); diff --git a/packages/pro-components/chat/chatbot/core/processor/index.ts b/packages/pro-components/chat/chatbot/core/processor/index.ts index 7668aff3c0..634a1dc6c6 100644 --- a/packages/pro-components/chat/chatbot/core/processor/index.ts +++ b/packages/pro-components/chat/chatbot/core/processor/index.ts @@ -155,3 +155,31 @@ export default class MessageProcessor { ); } } + +// // ... existing code ... +// import { strategyRegistry } from '../strategy/strategy-registry'; + +// export default class MessageProcessor { +// // 移除原有的 contentHandlers + +// public processContentUpdate( +// lastContent: AIMessageContent | undefined, +// newChunk: AIMessageContent +// ): AIMessageContent { +// // 获取对应类型的策略 +// const strategy = strategyRegistry.get(newChunk.type); + +// if (strategy && lastContent?.type === newChunk.type) { +// return strategy(newChunk, lastContent); +// } + +// // 没有策略时的默认合并逻辑 +// return { +// ...(lastContent || {}), +// ...newChunk, +// status: newChunk?.status || 'streaming' +// }; +// } + +// // 移除原有的 registerHandler 方法 +// } diff --git a/packages/pro-components/chat/chatbot/core/processor/registry.ts b/packages/pro-components/chat/chatbot/core/processor/registry.ts new file mode 100644 index 0000000000..4a176cd5c0 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/processor/registry.ts @@ -0,0 +1,45 @@ +// strategy-registry.ts +export type MergeStrategy = (chunk: T, existing?: T) => T; + +export class StrategyRegistry { + private strategies = new Map>(); + + register(type: T['type'], strategy: MergeStrategy) { + this.strategies.set(type, strategy); + } + + get(type: T['type']): MergeStrategy | null { + return this.strategies.get(type) || null; + } + + has(type: string): boolean { + return this.strategies.has(type); + } +} + +// 单例实例 +export const strategyRegistry = new StrategyRegistry(); + +// 文本类内容合并策略(text/markdown) +const textMergeStrategy: MergeStrategy = (chunk, existing) => ({ + ...(existing || chunk), + data: (existing?.data || '') + (chunk.data || ''), + status: chunk.status || 'streaming', +}); + +// 搜索类内容合并策略 +const searchMergeStrategy: MergeStrategy = (chunk, existing) => ({ + ...(existing || {}), + ...chunk, + data: { + ...(existing?.data || {}), + ...chunk.data, + references: [...(existing?.data?.references || []), ...(chunk.data?.references || [])], + }, +}); + +// 注册默认策略 +strategyRegistry.register('text', textMergeStrategy); +strategyRegistry.register('markdown', textMergeStrategy); +strategyRegistry.register('search', searchMergeStrategy); +strategyRegistry.register('thinking', textMergeStrategy); From 668b80a1866f80b82924a37c588cb61ed028e7ca Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 21 Jul 2025 11:20:30 +0800 Subject: [PATCH 119/228] feat(chatbot): reactify support reactnode props, chatbot support nostream fetch --- .../pro-components/chat/_util/reactify.tsx | 382 ++++++++++++---- .../pro-components/chat/_util/reactify_v1.tsx | 247 +++++++++++ .../pro-components/chat/_util/reactify_v2.tsx | 411 ++++++++++++++++++ .../chat/chat-message/_example/custom.tsx | 4 +- .../chat/chat-message/chat-message.md | 4 +- .../chat/chatbot/_example/basic.tsx | 2 +- .../chat/chatbot/_example/docs.tsx | 2 +- .../chat/chatbot/_example/hookComponent.tsx | 5 +- .../chat/chatbot/_example/nostream.tsx | 118 +++++ .../pro-components/chat/chatbot/chatbot.md | 8 +- packages/tdesign-react-aigc/package.json | 4 +- .../tdesign-react/site/plugin-tdoc/index.js | 39 +- 12 files changed, 1105 insertions(+), 121 deletions(-) create mode 100644 packages/pro-components/chat/_util/reactify_v1.tsx create mode 100644 packages/pro-components/chat/_util/reactify_v2.tsx create mode 100644 packages/pro-components/chat/chatbot/_example/nostream.tsx diff --git a/packages/pro-components/chat/_util/reactify.tsx b/packages/pro-components/chat/_util/reactify.tsx index 8ad0f594a8..3665841190 100644 --- a/packages/pro-components/chat/_util/reactify.tsx +++ b/packages/pro-components/chat/_util/reactify.tsx @@ -1,38 +1,44 @@ -import React, { Component, createRef, createElement, forwardRef } from 'react'; -import ReactDOM from 'react-dom'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +const { Component, createRef, createElement, forwardRef } = React; import { createRoot } from 'react-dom/client'; -// 检测 React 版本 -const isReact19Plus = (): boolean => { - const majorVersion = parseInt(React.version.split('.')[0]); - return majorVersion >= 19; -}; - // 检测 React 版本 const isReact18Plus = () => typeof createRoot !== 'undefined'; -// 缓存root实例的WeakMap -const rootCache = new WeakMap>(); +// 增强版本的缓存管理 +const rootCache = new WeakMap< + HTMLElement, + { + root: ReturnType; + lastElement?: React.ReactElement; + } +>(); -// 创建渲染函数 const createRenderer = (container: HTMLElement) => { if (isReact18Plus()) { - // 检查是否已有缓存的root实例 - let root = rootCache.get(container); - if (!root) { - root = createRoot(container); - rootCache.set(container, root); + let cached = rootCache.get(container); + if (!cached) { + cached = { root: createRoot(container) }; + rootCache.set(container, cached); } + return { render: (element: React.ReactElement) => { - root.render(element); + // 可选:避免相同元素的重复渲染 + if (cached.lastElement !== element) { + cached.root.render(element); + cached.lastElement = element; + } }, unmount: () => { - root.unmount(); + cached.root.unmount(); rootCache.delete(container); }, }; } + + // React 17的实现 return { render: (element: React.ReactElement) => { ReactDOM.render(element, container); @@ -43,21 +49,19 @@ const createRenderer = (container: HTMLElement) => { }; }; -const isFunctionComponentWithHooks = (component: any): boolean => { - // 1. 先检查是否是函数 - if (typeof component !== 'function') return false; - - // 2. 检查函数体是否包含 Hook 关键字 - const componentCode = component.toString(); - const hookKeywords = ['useState', 'useEffect', 'useRef', 'useContext', 'useMemo', 'useCallback', 'useReducer']; - - return hookKeywords.some((hook) => componentCode.includes(hook)); -}; +// 检查是否是React元素 +const isReactElement = (obj: any): obj is React.ReactElement => + obj && typeof obj === 'object' && obj.$$typeof && obj.$$typeof.toString().includes('react'); -const isClassComponent = (component: any): boolean => { - const isFC = typeof component === 'function'; - return !!(isFC && component.prototype?.render); -}; +// 检查是否是有效的React节点 +const isValidReactNode = (node: any): node is React.ReactNode => + node !== null && + node !== undefined && + (typeof node === 'string' || + typeof node === 'number' || + typeof node === 'boolean' || + isReactElement(node) || + Array.isArray(node)); type AnyProps = { [key: string]: any; @@ -69,7 +73,9 @@ export function hyphenate(str: string): string { return str.replace(hyphenateRE, '-$1').toLowerCase(); } -const styleObjectToString = (style: CSSRule) => { +const styleObjectToString = (style: any) => { + if (!style || typeof style !== 'object') return ''; + const unitlessKeys = new Set([ 'animationIterationCount', 'boxFlex', @@ -93,7 +99,7 @@ const styleObjectToString = (style: CSSRule) => { ]); return Object.entries(style) - .filter(([_, value]) => value != null && value !== '') // 过滤无效值 + .filter(([, value]) => value != null && value !== '') // 过滤无效值 .map(([key, value]) => { // 转换驼峰式为连字符格式 const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); @@ -115,14 +121,14 @@ const reactify = ( class Reactify extends Component { eventHandlers: [string, EventListener][]; - renderUnmountHandlers: Map void>; + slotRenderers: Map void>; ref: React.RefObject; - constructor(props: any) { + constructor(props: AnyProps) { super(props); this.eventHandlers = []; - this.renderUnmountHandlers = new Map(); + this.slotRenderers = new Map(); const { innerRef } = props; this.ref = innerRef || createRef(); } @@ -132,84 +138,271 @@ const reactify = ( this.ref.current?.addEventListener(event, val); } + // 防止重复处理的标记 + private processingSlots = new Set(); + + // 处理slot相关的prop + handleSlotProp(prop: string, val: any) { + const webComponent = this.ref.current as any; + if (!webComponent) return; + + // 防止重复处理同一个slot + if (this.processingSlots.has(prop)) { + return; + } + + // 检查是否需要更新(避免相同内容的重复渲染) + const currentRenderer = this.slotRenderers.get(prop); + if (currentRenderer && this.isSameReactElement(prop, val)) { + return; // 相同内容,跳过更新 + } + + // 标记正在处理 + this.processingSlots.add(prop); + + // 立即缓存新元素,防止重复调用 + if (isValidReactNode(val)) { + this.lastRenderedElements.set(prop, val); + } + + // 清理旧的渲染器 + if (currentRenderer) { + this.cleanupSlotRenderer(prop); + } + + // 如果val是函数,为WebComponent提供一个函数,该函数返回渲染后的DOM + if (typeof val === 'function') { + const renderSlot = (params?: any) => { + const reactNode = val(params); + return this.renderReactNodeToSlot(reactNode, prop); + }; + webComponent[prop] = renderSlot; + // 函数类型处理完成后立即移除标记 + this.processingSlots.delete(prop); + } + // 如果val是ReactNode,直接渲染到slot + else if (isValidReactNode(val)) { + // 先设置属性,让组件知道这个prop有值 + webComponent[prop] = true; + + // 使用微任务延迟渲染,确保在当前渲染周期完成后执行 + Promise.resolve().then(() => { + if (webComponent.update) { + webComponent.update(); + } + this.renderReactNodeToSlot(val, prop); + // 渲染完成后移除处理标记 + this.processingSlots.delete(prop); + }); + } + } + + // 清理slot渲染器的统一方法 + private cleanupSlotRenderer(slotName: string) { + const renderer = this.slotRenderers.get(slotName); + if (!renderer) return; + + // 立即清理DOM容器 + this.clearSlotContainers(slotName); + + // 总是异步清理React渲染器,避免竞态条件 + Promise.resolve().then(() => { + this.safeCleanupRenderer(renderer); + }); + + this.slotRenderers.delete(slotName); + } + + // 安全清理渲染器 + private safeCleanupRenderer(cleanup: () => void) { + try { + cleanup(); + } catch (error) { + console.warn('Error cleaning up React renderer:', error); + } + } + + // 立即清理指定slot的所有容器 + private clearSlotContainers(slotName: string) { + const webComponent = this.ref.current; + if (!webComponent) return; + + // 查找并移除所有匹配的slot容器 + const containers = webComponent.querySelectorAll(`[slot="${slotName}"]`); + containers.forEach((container: Element) => { + if (container.parentNode) { + container.parentNode.removeChild(container); + } + }); + } + + // 缓存最后渲染的React元素,用于比较 + private lastRenderedElements = new Map(); + + // 检查是否是相同的React元素 + private isSameReactElement(prop: string, val: any): boolean { + const lastElement = this.lastRenderedElements.get(prop); + + if (!lastElement || !isValidReactNode(val)) { + return false; + } + + // 简单比较:如果是相同的React元素引用,则认为相同 + if (lastElement === val) { + return true; + } + + // 对于React元素,比较type、key和props + if (React.isValidElement(lastElement) && React.isValidElement(val)) { + const typeMatch = lastElement.type === val.type; + const keyMatch = lastElement.key === val.key; + const propsMatch = JSON.stringify(lastElement.props) === JSON.stringify(val.props); + return typeMatch && keyMatch && propsMatch; + } + + return false; + } + + // 将React节点渲染到slot中 + renderReactNodeToSlot(reactNode: React.ReactNode, slotName: string) { + const webComponent = this.ref.current; + if (!webComponent) return; + + // 检查是否已经有相同的slot容器存在,避免重复创建 + const existingContainers = webComponent.querySelectorAll(`[slot="${slotName}"]`); + if (existingContainers.length > 0) { + return; + } + + // 直接创建容器并添加到Web Component中 + const container = document.createElement('div'); + container.style.display = 'contents'; // 不影响布局 + container.setAttribute('slot', slotName); // 设置slot属性,Web Components会自动处理 + + // 将容器添加到Web Component中 + webComponent.appendChild(container); + + // 根据不同类型的reactNode创建不同的清理函数 + let cleanupFn: (() => void) | null = null; + + if (isValidReactNode(reactNode)) { + if (React.isValidElement(reactNode)) { + try { + const renderer = createRenderer(container); + renderer.render(reactNode); + cleanupFn = () => { + try { + renderer.unmount(); + } catch (error) { + console.warn('Error unmounting React renderer:', error); + } + }; + } catch (error) { + console.warn('Error creating React renderer:', error); + } + } else if (typeof reactNode === 'string' || typeof reactNode === 'number') { + container.textContent = String(reactNode); + cleanupFn = () => { + container.textContent = ''; + }; + } else if (Array.isArray(reactNode)) { + try { + const renderer = createRenderer(container); + const wrapper = React.createElement( + 'div', + { style: { display: 'contents' } }, + ...reactNode.filter(isValidReactNode), + ); + renderer.render(wrapper); + cleanupFn = () => { + try { + renderer.unmount(); + } catch (error) { + console.warn('Error unmounting React renderer:', error); + } + }; + } catch (error) { + console.warn('Error creating React renderer for array:', error); + } + } + } + + // 保存cleanup函数 + this.slotRenderers.set(slotName, () => { + // 清理缓存 + this.lastRenderedElements.delete(slotName); + // 异步unmount避免竞态条件 + Promise.resolve().then(() => { + if (cleanupFn) { + cleanupFn(); + } + if (container.parentNode) { + container.parentNode.removeChild(container); + } + }); + }); + } + update() { this.clearEventHandlers(); if (!this.ref.current) return; + Object.entries(this.props).forEach(([prop, val]) => { if (['innerRef', 'children'].includes(prop)) return; - // event handler - if (typeof val === 'function') { - if (prop.match(/^on[A-Za-z]/)) { - const eventName = prop.slice(2); - const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); - this.setEvent(omiEventName, val); - } else if (prop.match(/^render[A-Za-z]/)) { - // Handle React function component - const ReactComponent = val; - const renderComponent = (params?: any, container?: any) => { - // params - // 重新render先unmount old - // if(this.renderUnmountHandlers.get(prop)){ - // this.renderUnmountHandlers.get(prop)?.(); - // } - - const component = - isFunctionComponentWithHooks(val) || isClassComponent(val) ? ( - - ) : ( - ReactComponent(params) - ); - - const renderer = createRenderer(container || document.createElement('div')); - renderer.render(component); - - // this.renderUnmountHandlers.set(prop, renderer.unmount); - - return container; - }; - (this.ref.current as any)[prop] = renderComponent; - } else if (!isReact19Plus()) { - // 其他函数 - (this.ref.current as any)[prop] = val; - } + // event handler + if (typeof val === 'function' && prop.match(/^on[A-Za-z]/)) { + const eventName = prop.slice(2); + const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); + this.setEvent(omiEventName, val as EventListener); return; } - // Complex object - if (typeof val === 'object') { - if (val?.$$typeof?.toString().match(/react/)) { - const renderComponent = (container?: any) => { - // 重新render先unmount old - // if(this.renderUnmountHandlers.get(prop)){ - // this.renderUnmountHandlers.get(prop)?.(); - // } - const renderer = createRenderer(container || document.createElement('div')); - renderer.render(val); - - // this.renderUnmountHandlers.set(prop, renderer.unmount); + // render functions or slot props + if (typeof val === 'function' && prop.match(/^render[A-Za-z]/)) { + this.handleSlotProp(prop, val); + return; + } - return container; - }; + // 检查是否是slot prop(通过组件的slotProps静态属性或Slot后缀) + if (isReactElement(val) && !prop.match(/^on[A-Za-z]/) && !prop.match(/^render[A-Za-z]/)) { + const componentClass = this.ref.current?.constructor as any; + const declaredSlots = componentClass?.slotProps || []; - (this.ref.current as any)[prop] = renderComponent; + if (declaredSlots.includes(prop) || prop.endsWith('Slot')) { + this.handleSlotProp(prop, val); return; } + } + + // Complex object处理 + if (typeof val === 'object' && val !== null) { + // style特殊处理 if (prop === 'style') { this.ref.current?.setAttribute('style', styleObjectToString(val)); return; } + // 其他复杂对象直接设置为属性 + (this.ref.current as any)[prop] = val; + + return; + } + + // 函数类型但不是事件处理器也不是render函数的,直接设置为属性 + if (typeof val === 'function') { (this.ref.current as any)[prop] = val; return; } - // camel case + + // camel case to kebab-case for attributes if (prop.match(hyphenateRE)) { this.ref.current?.setAttribute(hyphenate(prop), val); this.ref.current?.removeAttribute(prop); return; } - return; + // default: set as property + (this.ref.current as any)[prop] = val; }); } @@ -223,6 +416,7 @@ const reactify = ( componentWillUnmount() { this.clearEventHandlers(); + this.clearSlotRenderers(); } clearEventHandlers() { @@ -232,6 +426,14 @@ const reactify = ( this.eventHandlers = []; } + clearSlotRenderers() { + this.slotRenderers.forEach((cleanup) => { + this.safeCleanupRenderer(cleanup); + }); + this.slotRenderers.clear(); + this.processingSlots.clear(); + } + render() { const { children, className, innerRef, ...rest } = this.props; @@ -239,7 +441,7 @@ const reactify = ( } } - return forwardRef((props, ref) => + return forwardRef((props, ref) => createElement(Reactify, { ...props, innerRef: ref }), ) as React.ForwardRefExoticComponent & React.RefAttributes>; }; diff --git a/packages/pro-components/chat/_util/reactify_v1.tsx b/packages/pro-components/chat/_util/reactify_v1.tsx new file mode 100644 index 0000000000..8ad0f594a8 --- /dev/null +++ b/packages/pro-components/chat/_util/reactify_v1.tsx @@ -0,0 +1,247 @@ +import React, { Component, createRef, createElement, forwardRef } from 'react'; +import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; + +// 检测 React 版本 +const isReact19Plus = (): boolean => { + const majorVersion = parseInt(React.version.split('.')[0]); + return majorVersion >= 19; +}; + +// 检测 React 版本 +const isReact18Plus = () => typeof createRoot !== 'undefined'; + +// 缓存root实例的WeakMap +const rootCache = new WeakMap>(); + +// 创建渲染函数 +const createRenderer = (container: HTMLElement) => { + if (isReact18Plus()) { + // 检查是否已有缓存的root实例 + let root = rootCache.get(container); + if (!root) { + root = createRoot(container); + rootCache.set(container, root); + } + return { + render: (element: React.ReactElement) => { + root.render(element); + }, + unmount: () => { + root.unmount(); + rootCache.delete(container); + }, + }; + } + return { + render: (element: React.ReactElement) => { + ReactDOM.render(element, container); + }, + unmount: () => { + ReactDOM.unmountComponentAtNode(container); + }, + }; +}; + +const isFunctionComponentWithHooks = (component: any): boolean => { + // 1. 先检查是否是函数 + if (typeof component !== 'function') return false; + + // 2. 检查函数体是否包含 Hook 关键字 + const componentCode = component.toString(); + const hookKeywords = ['useState', 'useEffect', 'useRef', 'useContext', 'useMemo', 'useCallback', 'useReducer']; + + return hookKeywords.some((hook) => componentCode.includes(hook)); +}; + +const isClassComponent = (component: any): boolean => { + const isFC = typeof component === 'function'; + return !!(isFC && component.prototype?.render); +}; + +type AnyProps = { + [key: string]: any; +}; + +const hyphenateRE = /\B([A-Z])/g; + +export function hyphenate(str: string): string { + return str.replace(hyphenateRE, '-$1').toLowerCase(); +} + +const styleObjectToString = (style: CSSRule) => { + const unitlessKeys = new Set([ + 'animationIterationCount', + 'boxFlex', + 'boxFlexGroup', + 'boxOrdinalGroup', + 'columnCount', + 'fillOpacity', + 'flex', + 'flexGrow', + 'flexShrink', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', + ]); + + return Object.entries(style) + .filter(([_, value]) => value != null && value !== '') // 过滤无效值 + .map(([key, value]) => { + // 转换驼峰式为连字符格式 + const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); + + // 处理数值类型值 + let cssValue = value; + if (typeof value === 'number' && value !== 0 && !unitlessKeys.has(key)) { + cssValue = `${value}px`; + } + + return `${cssKey}:${cssValue};`; + }) + .join(' '); +}; + +const reactify = ( + WC: string, +): React.ForwardRefExoticComponent & React.RefAttributes> => { + class Reactify extends Component { + eventHandlers: [string, EventListener][]; + + renderUnmountHandlers: Map void>; + + ref: React.RefObject; + + constructor(props: any) { + super(props); + this.eventHandlers = []; + this.renderUnmountHandlers = new Map(); + const { innerRef } = props; + this.ref = innerRef || createRef(); + } + + setEvent(event: string, val: EventListener) { + this.eventHandlers.push([event, val]); + this.ref.current?.addEventListener(event, val); + } + + update() { + this.clearEventHandlers(); + if (!this.ref.current) return; + Object.entries(this.props).forEach(([prop, val]) => { + if (['innerRef', 'children'].includes(prop)) return; + // event handler + if (typeof val === 'function') { + if (prop.match(/^on[A-Za-z]/)) { + const eventName = prop.slice(2); + const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); + this.setEvent(omiEventName, val); + } else if (prop.match(/^render[A-Za-z]/)) { + // Handle React function component + const ReactComponent = val; + const renderComponent = (params?: any, container?: any) => { + // params + // 重新render先unmount old + // if(this.renderUnmountHandlers.get(prop)){ + // this.renderUnmountHandlers.get(prop)?.(); + // } + + const component = + isFunctionComponentWithHooks(val) || isClassComponent(val) ? ( + + ) : ( + ReactComponent(params) + ); + + const renderer = createRenderer(container || document.createElement('div')); + renderer.render(component); + + // this.renderUnmountHandlers.set(prop, renderer.unmount); + + return container; + }; + + (this.ref.current as any)[prop] = renderComponent; + } else if (!isReact19Plus()) { + // 其他函数 + (this.ref.current as any)[prop] = val; + } + return; + } + // Complex object + if (typeof val === 'object') { + if (val?.$$typeof?.toString().match(/react/)) { + const renderComponent = (container?: any) => { + // 重新render先unmount old + // if(this.renderUnmountHandlers.get(prop)){ + // this.renderUnmountHandlers.get(prop)?.(); + // } + + const renderer = createRenderer(container || document.createElement('div')); + renderer.render(val); + + // this.renderUnmountHandlers.set(prop, renderer.unmount); + + return container; + }; + + (this.ref.current as any)[prop] = renderComponent; + return; + } + if (prop === 'style') { + this.ref.current?.setAttribute('style', styleObjectToString(val)); + return; + } + (this.ref.current as any)[prop] = val; + return; + } + // camel case + if (prop.match(hyphenateRE)) { + this.ref.current?.setAttribute(hyphenate(prop), val); + this.ref.current?.removeAttribute(prop); + return; + } + + return; + }); + } + + componentDidUpdate() { + this.update(); + } + + componentDidMount() { + this.update(); + } + + componentWillUnmount() { + this.clearEventHandlers(); + } + + clearEventHandlers() { + this.eventHandlers.forEach(([event, handler]) => { + this.ref.current?.removeEventListener(event, handler); + }); + this.eventHandlers = []; + } + + render() { + const { children, className, innerRef, ...rest } = this.props; + + return createElement(WC, { class: className, ...rest, ref: this.ref }, children); + } + } + + return forwardRef((props, ref) => + createElement(Reactify, { ...props, innerRef: ref }), + ) as React.ForwardRefExoticComponent & React.RefAttributes>; +}; + +export default reactify; diff --git a/packages/pro-components/chat/_util/reactify_v2.tsx b/packages/pro-components/chat/_util/reactify_v2.tsx new file mode 100644 index 0000000000..a1e6599a36 --- /dev/null +++ b/packages/pro-components/chat/_util/reactify_v2.tsx @@ -0,0 +1,411 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +const { Component, createRef, createElement, forwardRef } = React; +import { createRoot } from 'react-dom/client'; + +// 检测 React 版本 +const isReact18Plus = () => typeof createRoot !== 'undefined'; + +// 增强版本的缓存管理 +const rootCache = new WeakMap< + HTMLElement, + { + root: ReturnType; + lastElement?: React.ReactElement; + } +>(); + +const createRenderer = (container: HTMLElement) => { + if (isReact18Plus()) { + let cached = rootCache.get(container); + if (!cached) { + cached = { root: createRoot(container) }; + rootCache.set(container, cached); + } + + return { + render: (element: React.ReactElement) => { + // 可选:避免相同元素的重复渲染 + if (cached.lastElement !== element) { + cached.root.render(element); + cached.lastElement = element; + } + }, + unmount: () => { + cached.root.unmount(); + rootCache.delete(container); + }, + }; + } + + // React 17的实现 + return { + render: (element: React.ReactElement) => { + ReactDOM.render(element, container); + }, + unmount: () => { + ReactDOM.unmountComponentAtNode(container); + }, + }; +}; + +// 检查是否是React元素 +const isReactElement = (obj: any): obj is React.ReactElement => + obj && typeof obj === 'object' && obj.$$typeof && obj.$$typeof.toString().includes('react'); + +// 检查是否是有效的React节点 +const isValidReactNode = (node: any): node is React.ReactNode => + node !== null && + node !== undefined && + (typeof node === 'string' || + typeof node === 'number' || + typeof node === 'boolean' || + isReactElement(node) || + Array.isArray(node)); + +type AnyProps = { + [key: string]: any; +}; + +const hyphenateRE = /\B([A-Z])/g; + +export function hyphenate(str: string): string { + return str.replace(hyphenateRE, '-$1').toLowerCase(); +} + +const styleObjectToString = (style: any) => { + if (!style || typeof style !== 'object') return ''; + + const unitlessKeys = new Set([ + 'animationIterationCount', + 'boxFlex', + 'boxFlexGroup', + 'boxOrdinalGroup', + 'columnCount', + 'fillOpacity', + 'flex', + 'flexGrow', + 'flexShrink', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', + ]); + + return Object.entries(style) + .filter(([, value]) => value != null && value !== '') // 过滤无效值 + .map(([key, value]) => { + // 转换驼峰式为连字符格式 + const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); + + // 处理数值类型值 + let cssValue = value; + if (typeof value === 'number' && value !== 0 && !unitlessKeys.has(key)) { + cssValue = `${value}px`; + } + + return `${cssKey}:${cssValue};`; + }) + .join(' '); +}; + +const reactify = ( + WC: string, +): React.ForwardRefExoticComponent & React.RefAttributes> => { + class Reactify extends Component { + eventHandlers: [string, EventListener][]; + + slotRenderers: Map void>; + + ref: React.RefObject; + + constructor(props: AnyProps) { + super(props); + this.eventHandlers = []; + this.slotRenderers = new Map(); + const { innerRef } = props; + this.ref = innerRef || createRef(); + } + + setEvent(event: string, val: EventListener) { + this.eventHandlers.push([event, val]); + this.ref.current?.addEventListener(event, val); + } + + // 处理slot相关的prop + handleSlotProp(prop: string, val: any) { + const webComponent = this.ref.current as any; + if (!webComponent) return; + + // 检查是否需要更新(避免相同内容的重复渲染) + const currentRenderer = this.slotRenderers.get(prop); + if (currentRenderer && this.isSameReactElement(prop, val)) { + return; // 相同内容,跳过更新 + } + + // 立即清理旧的DOM容器和React渲染器 + if (currentRenderer) { + // 立即清理DOM容器 + this.clearSlotContainers(prop); + // 立即清理React渲染器,但包装在try-catch中 + try { + currentRenderer(); + } catch (error) { + // 如果在React渲染过程中清理失败,使用异步清理 + Promise.resolve().then(() => { + try { + currentRenderer(); + } catch (e) { + console.warn('Error in async cleanup:', e); + } + }); + } + this.slotRenderers.delete(prop); + } + + // 如果val是函数,为WebComponent提供一个函数,该函数返回渲染后的DOM + if (typeof val === 'function') { + const renderSlot = (params?: any) => { + const reactNode = val(params); + return this.renderReactNodeToSlot(reactNode, prop); + }; + webComponent[prop] = renderSlot; + } + // 如果val是ReactNode,直接渲染到slot + else if (isValidReactNode(val)) { + // 先设置属性,让组件知道这个prop有值 + webComponent[prop] = true; + + // 立即更新组件并渲染,但在微任务中执行以避免阻塞 + Promise.resolve().then(() => { + if (webComponent.update) { + webComponent.update(); + } + this.renderReactNodeToSlot(val, prop); + }); + } + } + + // 立即清理指定slot的所有容器 + private clearSlotContainers(slotName: string) { + const webComponent = this.ref.current; + if (!webComponent) return; + + // 查找并移除所有匹配的slot容器 + const containers = webComponent.querySelectorAll(`[slot="${slotName}"]`); + containers.forEach((container: Element) => { + if (container.parentNode) { + container.parentNode.removeChild(container); + } + }); + } + + // 缓存最后渲染的React元素,用于比较 + private lastRenderedElements = new Map(); + + // 检查是否是相同的React元素 + private isSameReactElement(prop: string, val: any): boolean { + const lastElement = this.lastRenderedElements.get(prop); + if (!lastElement || !isValidReactNode(val)) { + return false; + } + + // 简单比较:如果是相同的React元素引用,则认为相同 + if (lastElement === val) { + return true; + } + + // 对于React元素,比较type和key + if (React.isValidElement(lastElement) && React.isValidElement(val)) { + return lastElement.type === val.type && lastElement.key === val.key; + } + + return false; + } + + // 将React节点渲染到slot中 + renderReactNodeToSlot(reactNode: React.ReactNode, slotName: string) { + const webComponent = this.ref.current; + if (!webComponent) return; + + // 缓存当前渲染的元素 + this.lastRenderedElements.set(slotName, reactNode); + + // 直接创建容器并添加到Web Component中 + const container = document.createElement('div'); + container.style.display = 'contents'; // 不影响布局 + container.setAttribute('slot', slotName); // 设置slot属性,Web Components会自动处理 + + // 将容器添加到Web Component中 + webComponent.appendChild(container); + + // 根据不同类型的reactNode创建不同的清理函数 + let cleanupFn: (() => void) | null = null; + + if (isValidReactNode(reactNode)) { + if (React.isValidElement(reactNode)) { + try { + const renderer = createRenderer(container); + renderer.render(reactNode); + cleanupFn = () => { + try { + renderer.unmount(); + } catch (error) { + console.warn('Error unmounting React renderer:', error); + } + }; + } catch (error) { + console.warn('Error creating React renderer:', error); + } + } else if (typeof reactNode === 'string' || typeof reactNode === 'number') { + container.textContent = String(reactNode); + cleanupFn = () => { + container.textContent = ''; + }; + } else if (Array.isArray(reactNode)) { + try { + const renderer = createRenderer(container); + const wrapper = React.createElement( + 'div', + { style: { display: 'contents' } }, + ...reactNode.filter(isValidReactNode), + ); + renderer.render(wrapper); + cleanupFn = () => { + try { + renderer.unmount(); + } catch (error) { + console.warn('Error unmounting React renderer:', error); + } + }; + } catch (error) { + console.warn('Error creating React renderer for array:', error); + } + } + } + + // 保存cleanup函数 + this.slotRenderers.set(slotName, () => { + // 清理缓存 + this.lastRenderedElements.delete(slotName); + // 异步unmount避免竞态条件 + Promise.resolve().then(() => { + if (cleanupFn) { + cleanupFn(); + } + if (container.parentNode) { + container.parentNode.removeChild(container); + } + }); + }); + } + + update() { + this.clearEventHandlers(); + if (!this.ref.current) return; + + Object.entries(this.props).forEach(([prop, val]) => { + if (['innerRef', 'children'].includes(prop)) return; + + // event handler + if (typeof val === 'function' && prop.match(/^on[A-Za-z]/)) { + const eventName = prop.slice(2); + const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); + this.setEvent(omiEventName, val as EventListener); + return; + } + + // render functions or slot props + if (typeof val === 'function' && prop.match(/^render[A-Za-z]/)) { + this.handleSlotProp(prop, val); + return; + } + + // 检查是否是slot prop(通过组件的slotProps静态属性或Slot后缀) + if (isReactElement(val) && !prop.match(/^on[A-Za-z]/) && !prop.match(/^render[A-Za-z]/)) { + const componentClass = this.ref.current?.constructor as any; + const declaredSlots = componentClass?.slotProps || []; + + if (declaredSlots.includes(prop) || prop.endsWith('Slot')) { + this.handleSlotProp(prop, val); + return; + } + } + + // Complex object处理 + if (typeof val === 'object' && val !== null) { + // style特殊处理 + if (prop === 'style') { + this.ref.current?.setAttribute('style', styleObjectToString(val)); + return; + } + // 其他复杂对象直接设置为属性 + (this.ref.current as any)[prop] = val; + + return; + } + + // 函数类型但不是事件处理器也不是render函数的,直接设置为属性 + if (typeof val === 'function') { + (this.ref.current as any)[prop] = val; + return; + } + + // camel case to kebab-case for attributes + if (prop.match(hyphenateRE)) { + this.ref.current?.setAttribute(hyphenate(prop), val); + this.ref.current?.removeAttribute(prop); + return; + } + + // default: set as property + (this.ref.current as any)[prop] = val; + }); + } + + componentDidUpdate() { + this.update(); + } + + componentDidMount() { + this.update(); + } + + componentWillUnmount() { + this.clearEventHandlers(); + this.clearSlotRenderers(); + } + + clearEventHandlers() { + this.eventHandlers.forEach(([event, handler]) => { + this.ref.current?.removeEventListener(event, handler); + }); + this.eventHandlers = []; + } + + clearSlotRenderers() { + this.slotRenderers.forEach((cleanup) => { + cleanup(); + }); + this.slotRenderers.clear(); + } + + render() { + const { children, className, innerRef, ...rest } = this.props; + + return createElement(WC, { class: className, ...rest, ref: this.ref }, children); + } + } + + return forwardRef((props, ref) => + createElement(Reactify, { ...props, innerRef: ref }), + ) as React.ForwardRefExoticComponent & React.RefAttributes>; +}; + +export default reactify; diff --git a/packages/pro-components/chat/chat-message/_example/custom.tsx b/packages/pro-components/chat/chat-message/_example/custom.tsx index c2c4ffbabd..78b476b7f2 100644 --- a/packages/pro-components/chat/chat-message/_example/custom.tsx +++ b/packages/pro-components/chat/chat-message/_example/custom.tsx @@ -1,6 +1,6 @@ import React from 'react'; import TvisionTcharts from 'tvision-charts-react'; -import { Space } from 'tdesign-react'; +import { Avatar, Space } from 'tdesign-react'; import { ChatBaseContent, ChatMessage } from '@tdesign-react/aigc'; @@ -81,7 +81,7 @@ export default function ChatMessageExample() { } name="TDesignAI" message={message} > diff --git a/packages/pro-components/chat/chat-message/chat-message.md b/packages/pro-components/chat/chat-message/chat-message.md index 9cb0686417..3bf6076a69 100644 --- a/packages/pro-components/chat/chat-message/chat-message.md +++ b/packages/pro-components/chat/chat-message/chat-message.md @@ -41,8 +41,8 @@ spline: aigc placement | String | left | 消息显示位置。可选项:left/right | N variant | String | text | 消息气泡样式变体。可选项:base/outline/text | N animation | String | skeleton | 加载动画类型。可选项:skeleton/moving/gradient/circle | N -name | String | - | 发送者名称 | N -avatar | String | - | 发送者头像 | N +name | String/ReactNode | - | 发送者名称 | N +avatar | String/ReactNode | - | 发送者头像 | N datetime | String | - | 消息发送时间 | N message | Object | - | 消息内容对象。类型定义见下方 `ChatMessagesData` | Y chatContentProps | Object | - | 消息内容属性配置。类型支持见 `chatContentProps` | N diff --git a/packages/pro-components/chat/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx index ffc99eb2ae..1d59738b10 100644 --- a/packages/pro-components/chat/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -105,7 +105,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal`, + endpoint: `http://localhost:3000/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/docs.tsx b/packages/pro-components/chat/chatbot/_example/docs.tsx index cc27e54282..6654e00550 100644 --- a/packages/pro-components/chat/chatbot/_example/docs.tsx +++ b/packages/pro-components/chat/chatbot/_example/docs.tsx @@ -165,7 +165,7 @@ export default function chatSample() { onFileSelect, onFileRemove, }} - onChatSent={onSend} + onChatAfterSend={onSend} chatServiceConfig={chatServiceConfig} >
diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 7911805582..f9c3dcf60b 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -1,4 +1,5 @@ import React, { ReactNode, useMemo, useRef, useState } from 'react'; +import { Avatar } from 'tdesign-react'; import { type SSEChunkData, type TdChatMessageConfig, @@ -13,8 +14,9 @@ import { ChatActionBar, isAIMessage, useChat, + getMessageContentForCopy, + TdChatSenderParams, } from '@tdesign-react/aigc'; -import { getMessageContentForCopy, TdChatActionsName, TdChatSenderParams } from 'tdesign-web-components'; import mockData from './mock/data'; export default function ComponentsBuild() { @@ -100,6 +102,7 @@ export default function ComponentsBuild() { user: { variant: 'base', placement: 'right', + avatar: , }, assistant: { placement: 'left', diff --git a/packages/pro-components/chat/chatbot/_example/nostream.tsx b/packages/pro-components/chat/chatbot/_example/nostream.tsx new file mode 100644 index 0000000000..87e6098e8b --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/nostream.tsx @@ -0,0 +1,118 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { InternetIcon } from 'tdesign-icons-react'; +import { + TdChatMessageConfigItem, + ChatRequestParams, + ChatMessagesData, + ChatServiceConfig, + ChatBot, + type TdChatbotApi, +} from '@tdesign-react/aigc'; +import { Button, Space } from 'tdesign-react'; + +export default function chatSample() { + const chatRef = useRef(null); + const [activeR1, setR1Active] = useState(false); + const [activeSearch, setSearchActive] = useState(false); + const [ready, setReady] = useState(false); + const reqParamsRef = useRef<{ think: boolean; search: boolean }>({ think: false, search: false }); + + // 消息属性配置 + const messageProps = (msg: ChatMessagesData): TdChatMessageConfigItem => { + const { role, content } = msg; + if (role === 'user') { + return { + variant: 'base', + placement: 'right', + avatar: 'https://tdesign.gtimg.com/site/avatar.jpg', + }; + } + if (role === 'assistant') { + return { + placement: 'left', + actions: ['replay', 'copy', 'good', 'bad'], + handleActions: { + // 处理消息操作回调 + good: async ({ message, active }) => { + // 点赞 + console.log('点赞', message, active); + }, + bad: async ({ message, active }) => { + // 点踩 + console.log('点踩', message, active); + }, + replay: ({ message, active }) => { + console.log('自定义重新回复', message, active); + chatRef?.current?.regenerate(); + }, + suggestion: ({ content }) => { + console.log('点击建议问题', content); + chatRef?.current?.sendUserMessage({ prompt: content.prompt }); + }, + }, + }; + } + }; + + // 聊天服务配置 + const chatServiceConfig: ChatServiceConfig = { + // 对话服务地址 + endpoint: `http://localhost:3000/fetch/normal`, + stream: false, + // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) + onComplete: (isAborted, req, result) => { + console.log('onComplete', isAborted, req, result); + return { + type: 'text', + data: result.data, + }; + }, + // 流式对话过程中出错业务自定义行为 + onError: (err: Error | Response) => { + console.error('Chatservice Error:', err); + }, + // 流式对话过程中用户主动结束对话业务自定义行为 + onAbort: async () => { + chatRef.current?.sendSystemMessage('用户已暂停'); + }, + // 自定义请求参数 + onRequest: (innerParams: ChatRequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'tdesign-chat', + prompt, + ...reqParamsRef.current, + }), + }; + }, + }; + + useEffect(() => { + reqParamsRef.current = { + think: activeR1, + search: activeSearch, + }; + }, [activeR1, activeSearch]); + + return ( +
+ { + setReady(true); + }} + > +
+ ); +} diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index 92305c1142..a09e64a785 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -55,6 +55,8 @@ spline: navigation 以下案例模拟了使用Chatbot搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,**自定义消息插槽名规则**,同时演示了**自定义任务流程渲染** {{ agent }} +### 非流式输出模式 +{{ nostream }} ## API ### Chatbot Props @@ -68,7 +70,7 @@ senderProps | Object | - | 发送框配置,透传`ChatSender`组件。TS类型 chatServiceConfig | Object | - | 聊天服务配置,见下方详细说明,TS类型:`ChatServiceConfig` | N onMessageChange | Function | - | 消息列表数据变化回调,TS类型:`(e: CustomEvent) => void` | N onChatReady | Function | - | 内部消息引擎初始化完成回调,TS类型:`(e: CustomEvent) => void` | N -onChatSent | Function | - | 发送消息回调,TS类型:`(e: CustomEvent) => void` | N +onChatAfterSend | Function | - | 发送消息回调,TS类型:`(e: CustomEvent) => void` | N ### TdChatListProps 消息列表配置 @@ -89,8 +91,8 @@ onScroll | Function | - | 滚动事件回调 | N endpoint | String | - | 聊天服务请求地址url | N stream | Boolean | true | 是否使用流式传输 | N onRequest | Function | - | 请求前的回调,可修改请求参数。TS类型:`(params: ChatRequestParams) => RequestInit` | N -onMessage | Function | - | 处理流式消息的回调。TS类型:`(chunk: SSEChunkData) => AIMessageContent / null` | N -onComplete | Function | - | 请求结束时的回调。TS类型:`(isAborted: boolean, params: RequestInit, result?: any) => void` | N +onMessage | Function | - | 处理流式消息的回调。TS类型:`(chunk: SSEChunkData) => AIMessageContent / AIMessageContent[] / null` | N +onComplete | Function | - | 请求结束时的回调。TS类型:`(isAborted: boolean, params: RequestInit, result?: any) => AIMessageContent / AIMessageContent[] / null` | N onAbort | Function | - | 中止请求时的回调。TS类型:`() => Promise` | N onError | Function | - | 错误处理回调。TS类型:`(err: Error \| Response) => void` | N diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 495dd8e88c..6464a46243 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,6 +1,6 @@ { "name": "@tdesign-react/aigc", - "version": "0.1.0-alpha.12", + "version": "0.1.0-alpha.13", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", @@ -50,7 +50,7 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.1.5", + "tdesign-web-components": "1.1.7", "classnames": "~2.5.1", "lodash-es": "^4.17.21" }, diff --git a/packages/tdesign-react/site/plugin-tdoc/index.js b/packages/tdesign-react/site/plugin-tdoc/index.js index 23740e9eb8..20d7c5652a 100644 --- a/packages/tdesign-react/site/plugin-tdoc/index.js +++ b/packages/tdesign-react/site/plugin-tdoc/index.js @@ -3,23 +3,24 @@ import vitePluginTdoc from 'vite-plugin-tdoc'; import transforms from './transforms'; import renderDemo from './demo'; -export default () => vitePluginTdoc({ - transforms, // 解析 markdown 数据 - markdown: { - anchor: { - tabIndex: false, - config: (anchor) => ({ - permalink: anchor.permalink.linkInsideHeader({ symbol: '' }), - }), +export default () => + vitePluginTdoc({ + transforms, // 解析 markdown 数据 + markdown: { + anchor: { + tabIndex: false, + config: (anchor) => ({ + permalink: anchor.permalink.linkInsideHeader({ symbol: '' }), + }), + }, + toc: { + listClass: 'tdesign-toc_list', + itemClass: 'tdesign-toc_list_item', + linkClass: 'tdesign-toc_list_item_a', + containerClass: 'tdesign-toc_container', + }, + container(md, container) { + renderDemo(md, container); + }, }, - toc: { - listClass: 'tdesign-toc_list', - itemClass: 'tdesign-toc_list_item', - linkClass: 'tdesign-toc_list_item_a', - containerClass: 'tdesign-toc_container', - }, - container(md, container) { - renderDemo(md, container); - }, - }, -}); + }); From 0aa0b76ddc77fa63f85c159d4446cd3f764c8305 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Mon, 21 Jul 2025 11:22:49 +0800 Subject: [PATCH 120/228] feat(reactify): support reactnode as props --- .../pro-components/chat/_util/reactify_v1.tsx | 247 ----------- .../pro-components/chat/_util/reactify_v2.tsx | 411 ------------------ 2 files changed, 658 deletions(-) delete mode 100644 packages/pro-components/chat/_util/reactify_v1.tsx delete mode 100644 packages/pro-components/chat/_util/reactify_v2.tsx diff --git a/packages/pro-components/chat/_util/reactify_v1.tsx b/packages/pro-components/chat/_util/reactify_v1.tsx deleted file mode 100644 index 8ad0f594a8..0000000000 --- a/packages/pro-components/chat/_util/reactify_v1.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import React, { Component, createRef, createElement, forwardRef } from 'react'; -import ReactDOM from 'react-dom'; -import { createRoot } from 'react-dom/client'; - -// 检测 React 版本 -const isReact19Plus = (): boolean => { - const majorVersion = parseInt(React.version.split('.')[0]); - return majorVersion >= 19; -}; - -// 检测 React 版本 -const isReact18Plus = () => typeof createRoot !== 'undefined'; - -// 缓存root实例的WeakMap -const rootCache = new WeakMap>(); - -// 创建渲染函数 -const createRenderer = (container: HTMLElement) => { - if (isReact18Plus()) { - // 检查是否已有缓存的root实例 - let root = rootCache.get(container); - if (!root) { - root = createRoot(container); - rootCache.set(container, root); - } - return { - render: (element: React.ReactElement) => { - root.render(element); - }, - unmount: () => { - root.unmount(); - rootCache.delete(container); - }, - }; - } - return { - render: (element: React.ReactElement) => { - ReactDOM.render(element, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - }, - }; -}; - -const isFunctionComponentWithHooks = (component: any): boolean => { - // 1. 先检查是否是函数 - if (typeof component !== 'function') return false; - - // 2. 检查函数体是否包含 Hook 关键字 - const componentCode = component.toString(); - const hookKeywords = ['useState', 'useEffect', 'useRef', 'useContext', 'useMemo', 'useCallback', 'useReducer']; - - return hookKeywords.some((hook) => componentCode.includes(hook)); -}; - -const isClassComponent = (component: any): boolean => { - const isFC = typeof component === 'function'; - return !!(isFC && component.prototype?.render); -}; - -type AnyProps = { - [key: string]: any; -}; - -const hyphenateRE = /\B([A-Z])/g; - -export function hyphenate(str: string): string { - return str.replace(hyphenateRE, '-$1').toLowerCase(); -} - -const styleObjectToString = (style: CSSRule) => { - const unitlessKeys = new Set([ - 'animationIterationCount', - 'boxFlex', - 'boxFlexGroup', - 'boxOrdinalGroup', - 'columnCount', - 'fillOpacity', - 'flex', - 'flexGrow', - 'flexShrink', - 'fontWeight', - 'lineClamp', - 'lineHeight', - 'opacity', - 'order', - 'orphans', - 'tabSize', - 'widows', - 'zIndex', - 'zoom', - ]); - - return Object.entries(style) - .filter(([_, value]) => value != null && value !== '') // 过滤无效值 - .map(([key, value]) => { - // 转换驼峰式为连字符格式 - const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); - - // 处理数值类型值 - let cssValue = value; - if (typeof value === 'number' && value !== 0 && !unitlessKeys.has(key)) { - cssValue = `${value}px`; - } - - return `${cssKey}:${cssValue};`; - }) - .join(' '); -}; - -const reactify = ( - WC: string, -): React.ForwardRefExoticComponent & React.RefAttributes> => { - class Reactify extends Component { - eventHandlers: [string, EventListener][]; - - renderUnmountHandlers: Map void>; - - ref: React.RefObject; - - constructor(props: any) { - super(props); - this.eventHandlers = []; - this.renderUnmountHandlers = new Map(); - const { innerRef } = props; - this.ref = innerRef || createRef(); - } - - setEvent(event: string, val: EventListener) { - this.eventHandlers.push([event, val]); - this.ref.current?.addEventListener(event, val); - } - - update() { - this.clearEventHandlers(); - if (!this.ref.current) return; - Object.entries(this.props).forEach(([prop, val]) => { - if (['innerRef', 'children'].includes(prop)) return; - // event handler - if (typeof val === 'function') { - if (prop.match(/^on[A-Za-z]/)) { - const eventName = prop.slice(2); - const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); - this.setEvent(omiEventName, val); - } else if (prop.match(/^render[A-Za-z]/)) { - // Handle React function component - const ReactComponent = val; - const renderComponent = (params?: any, container?: any) => { - // params - // 重新render先unmount old - // if(this.renderUnmountHandlers.get(prop)){ - // this.renderUnmountHandlers.get(prop)?.(); - // } - - const component = - isFunctionComponentWithHooks(val) || isClassComponent(val) ? ( - - ) : ( - ReactComponent(params) - ); - - const renderer = createRenderer(container || document.createElement('div')); - renderer.render(component); - - // this.renderUnmountHandlers.set(prop, renderer.unmount); - - return container; - }; - - (this.ref.current as any)[prop] = renderComponent; - } else if (!isReact19Plus()) { - // 其他函数 - (this.ref.current as any)[prop] = val; - } - return; - } - // Complex object - if (typeof val === 'object') { - if (val?.$$typeof?.toString().match(/react/)) { - const renderComponent = (container?: any) => { - // 重新render先unmount old - // if(this.renderUnmountHandlers.get(prop)){ - // this.renderUnmountHandlers.get(prop)?.(); - // } - - const renderer = createRenderer(container || document.createElement('div')); - renderer.render(val); - - // this.renderUnmountHandlers.set(prop, renderer.unmount); - - return container; - }; - - (this.ref.current as any)[prop] = renderComponent; - return; - } - if (prop === 'style') { - this.ref.current?.setAttribute('style', styleObjectToString(val)); - return; - } - (this.ref.current as any)[prop] = val; - return; - } - // camel case - if (prop.match(hyphenateRE)) { - this.ref.current?.setAttribute(hyphenate(prop), val); - this.ref.current?.removeAttribute(prop); - return; - } - - return; - }); - } - - componentDidUpdate() { - this.update(); - } - - componentDidMount() { - this.update(); - } - - componentWillUnmount() { - this.clearEventHandlers(); - } - - clearEventHandlers() { - this.eventHandlers.forEach(([event, handler]) => { - this.ref.current?.removeEventListener(event, handler); - }); - this.eventHandlers = []; - } - - render() { - const { children, className, innerRef, ...rest } = this.props; - - return createElement(WC, { class: className, ...rest, ref: this.ref }, children); - } - } - - return forwardRef((props, ref) => - createElement(Reactify, { ...props, innerRef: ref }), - ) as React.ForwardRefExoticComponent & React.RefAttributes>; -}; - -export default reactify; diff --git a/packages/pro-components/chat/_util/reactify_v2.tsx b/packages/pro-components/chat/_util/reactify_v2.tsx deleted file mode 100644 index a1e6599a36..0000000000 --- a/packages/pro-components/chat/_util/reactify_v2.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -const { Component, createRef, createElement, forwardRef } = React; -import { createRoot } from 'react-dom/client'; - -// 检测 React 版本 -const isReact18Plus = () => typeof createRoot !== 'undefined'; - -// 增强版本的缓存管理 -const rootCache = new WeakMap< - HTMLElement, - { - root: ReturnType; - lastElement?: React.ReactElement; - } ->(); - -const createRenderer = (container: HTMLElement) => { - if (isReact18Plus()) { - let cached = rootCache.get(container); - if (!cached) { - cached = { root: createRoot(container) }; - rootCache.set(container, cached); - } - - return { - render: (element: React.ReactElement) => { - // 可选:避免相同元素的重复渲染 - if (cached.lastElement !== element) { - cached.root.render(element); - cached.lastElement = element; - } - }, - unmount: () => { - cached.root.unmount(); - rootCache.delete(container); - }, - }; - } - - // React 17的实现 - return { - render: (element: React.ReactElement) => { - ReactDOM.render(element, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - }, - }; -}; - -// 检查是否是React元素 -const isReactElement = (obj: any): obj is React.ReactElement => - obj && typeof obj === 'object' && obj.$$typeof && obj.$$typeof.toString().includes('react'); - -// 检查是否是有效的React节点 -const isValidReactNode = (node: any): node is React.ReactNode => - node !== null && - node !== undefined && - (typeof node === 'string' || - typeof node === 'number' || - typeof node === 'boolean' || - isReactElement(node) || - Array.isArray(node)); - -type AnyProps = { - [key: string]: any; -}; - -const hyphenateRE = /\B([A-Z])/g; - -export function hyphenate(str: string): string { - return str.replace(hyphenateRE, '-$1').toLowerCase(); -} - -const styleObjectToString = (style: any) => { - if (!style || typeof style !== 'object') return ''; - - const unitlessKeys = new Set([ - 'animationIterationCount', - 'boxFlex', - 'boxFlexGroup', - 'boxOrdinalGroup', - 'columnCount', - 'fillOpacity', - 'flex', - 'flexGrow', - 'flexShrink', - 'fontWeight', - 'lineClamp', - 'lineHeight', - 'opacity', - 'order', - 'orphans', - 'tabSize', - 'widows', - 'zIndex', - 'zoom', - ]); - - return Object.entries(style) - .filter(([, value]) => value != null && value !== '') // 过滤无效值 - .map(([key, value]) => { - // 转换驼峰式为连字符格式 - const cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); - - // 处理数值类型值 - let cssValue = value; - if (typeof value === 'number' && value !== 0 && !unitlessKeys.has(key)) { - cssValue = `${value}px`; - } - - return `${cssKey}:${cssValue};`; - }) - .join(' '); -}; - -const reactify = ( - WC: string, -): React.ForwardRefExoticComponent & React.RefAttributes> => { - class Reactify extends Component { - eventHandlers: [string, EventListener][]; - - slotRenderers: Map void>; - - ref: React.RefObject; - - constructor(props: AnyProps) { - super(props); - this.eventHandlers = []; - this.slotRenderers = new Map(); - const { innerRef } = props; - this.ref = innerRef || createRef(); - } - - setEvent(event: string, val: EventListener) { - this.eventHandlers.push([event, val]); - this.ref.current?.addEventListener(event, val); - } - - // 处理slot相关的prop - handleSlotProp(prop: string, val: any) { - const webComponent = this.ref.current as any; - if (!webComponent) return; - - // 检查是否需要更新(避免相同内容的重复渲染) - const currentRenderer = this.slotRenderers.get(prop); - if (currentRenderer && this.isSameReactElement(prop, val)) { - return; // 相同内容,跳过更新 - } - - // 立即清理旧的DOM容器和React渲染器 - if (currentRenderer) { - // 立即清理DOM容器 - this.clearSlotContainers(prop); - // 立即清理React渲染器,但包装在try-catch中 - try { - currentRenderer(); - } catch (error) { - // 如果在React渲染过程中清理失败,使用异步清理 - Promise.resolve().then(() => { - try { - currentRenderer(); - } catch (e) { - console.warn('Error in async cleanup:', e); - } - }); - } - this.slotRenderers.delete(prop); - } - - // 如果val是函数,为WebComponent提供一个函数,该函数返回渲染后的DOM - if (typeof val === 'function') { - const renderSlot = (params?: any) => { - const reactNode = val(params); - return this.renderReactNodeToSlot(reactNode, prop); - }; - webComponent[prop] = renderSlot; - } - // 如果val是ReactNode,直接渲染到slot - else if (isValidReactNode(val)) { - // 先设置属性,让组件知道这个prop有值 - webComponent[prop] = true; - - // 立即更新组件并渲染,但在微任务中执行以避免阻塞 - Promise.resolve().then(() => { - if (webComponent.update) { - webComponent.update(); - } - this.renderReactNodeToSlot(val, prop); - }); - } - } - - // 立即清理指定slot的所有容器 - private clearSlotContainers(slotName: string) { - const webComponent = this.ref.current; - if (!webComponent) return; - - // 查找并移除所有匹配的slot容器 - const containers = webComponent.querySelectorAll(`[slot="${slotName}"]`); - containers.forEach((container: Element) => { - if (container.parentNode) { - container.parentNode.removeChild(container); - } - }); - } - - // 缓存最后渲染的React元素,用于比较 - private lastRenderedElements = new Map(); - - // 检查是否是相同的React元素 - private isSameReactElement(prop: string, val: any): boolean { - const lastElement = this.lastRenderedElements.get(prop); - if (!lastElement || !isValidReactNode(val)) { - return false; - } - - // 简单比较:如果是相同的React元素引用,则认为相同 - if (lastElement === val) { - return true; - } - - // 对于React元素,比较type和key - if (React.isValidElement(lastElement) && React.isValidElement(val)) { - return lastElement.type === val.type && lastElement.key === val.key; - } - - return false; - } - - // 将React节点渲染到slot中 - renderReactNodeToSlot(reactNode: React.ReactNode, slotName: string) { - const webComponent = this.ref.current; - if (!webComponent) return; - - // 缓存当前渲染的元素 - this.lastRenderedElements.set(slotName, reactNode); - - // 直接创建容器并添加到Web Component中 - const container = document.createElement('div'); - container.style.display = 'contents'; // 不影响布局 - container.setAttribute('slot', slotName); // 设置slot属性,Web Components会自动处理 - - // 将容器添加到Web Component中 - webComponent.appendChild(container); - - // 根据不同类型的reactNode创建不同的清理函数 - let cleanupFn: (() => void) | null = null; - - if (isValidReactNode(reactNode)) { - if (React.isValidElement(reactNode)) { - try { - const renderer = createRenderer(container); - renderer.render(reactNode); - cleanupFn = () => { - try { - renderer.unmount(); - } catch (error) { - console.warn('Error unmounting React renderer:', error); - } - }; - } catch (error) { - console.warn('Error creating React renderer:', error); - } - } else if (typeof reactNode === 'string' || typeof reactNode === 'number') { - container.textContent = String(reactNode); - cleanupFn = () => { - container.textContent = ''; - }; - } else if (Array.isArray(reactNode)) { - try { - const renderer = createRenderer(container); - const wrapper = React.createElement( - 'div', - { style: { display: 'contents' } }, - ...reactNode.filter(isValidReactNode), - ); - renderer.render(wrapper); - cleanupFn = () => { - try { - renderer.unmount(); - } catch (error) { - console.warn('Error unmounting React renderer:', error); - } - }; - } catch (error) { - console.warn('Error creating React renderer for array:', error); - } - } - } - - // 保存cleanup函数 - this.slotRenderers.set(slotName, () => { - // 清理缓存 - this.lastRenderedElements.delete(slotName); - // 异步unmount避免竞态条件 - Promise.resolve().then(() => { - if (cleanupFn) { - cleanupFn(); - } - if (container.parentNode) { - container.parentNode.removeChild(container); - } - }); - }); - } - - update() { - this.clearEventHandlers(); - if (!this.ref.current) return; - - Object.entries(this.props).forEach(([prop, val]) => { - if (['innerRef', 'children'].includes(prop)) return; - - // event handler - if (typeof val === 'function' && prop.match(/^on[A-Za-z]/)) { - const eventName = prop.slice(2); - const omiEventName = eventName[0].toLowerCase() + eventName.slice(1); - this.setEvent(omiEventName, val as EventListener); - return; - } - - // render functions or slot props - if (typeof val === 'function' && prop.match(/^render[A-Za-z]/)) { - this.handleSlotProp(prop, val); - return; - } - - // 检查是否是slot prop(通过组件的slotProps静态属性或Slot后缀) - if (isReactElement(val) && !prop.match(/^on[A-Za-z]/) && !prop.match(/^render[A-Za-z]/)) { - const componentClass = this.ref.current?.constructor as any; - const declaredSlots = componentClass?.slotProps || []; - - if (declaredSlots.includes(prop) || prop.endsWith('Slot')) { - this.handleSlotProp(prop, val); - return; - } - } - - // Complex object处理 - if (typeof val === 'object' && val !== null) { - // style特殊处理 - if (prop === 'style') { - this.ref.current?.setAttribute('style', styleObjectToString(val)); - return; - } - // 其他复杂对象直接设置为属性 - (this.ref.current as any)[prop] = val; - - return; - } - - // 函数类型但不是事件处理器也不是render函数的,直接设置为属性 - if (typeof val === 'function') { - (this.ref.current as any)[prop] = val; - return; - } - - // camel case to kebab-case for attributes - if (prop.match(hyphenateRE)) { - this.ref.current?.setAttribute(hyphenate(prop), val); - this.ref.current?.removeAttribute(prop); - return; - } - - // default: set as property - (this.ref.current as any)[prop] = val; - }); - } - - componentDidUpdate() { - this.update(); - } - - componentDidMount() { - this.update(); - } - - componentWillUnmount() { - this.clearEventHandlers(); - this.clearSlotRenderers(); - } - - clearEventHandlers() { - this.eventHandlers.forEach(([event, handler]) => { - this.ref.current?.removeEventListener(event, handler); - }); - this.eventHandlers = []; - } - - clearSlotRenderers() { - this.slotRenderers.forEach((cleanup) => { - cleanup(); - }); - this.slotRenderers.clear(); - } - - render() { - const { children, className, innerRef, ...rest } = this.props; - - return createElement(WC, { class: className, ...rest, ref: this.ref }, children); - } - } - - return forwardRef((props, ref) => - createElement(Reactify, { ...props, innerRef: ref }), - ) as React.ForwardRefExoticComponent & React.RefAttributes>; -}; - -export default reactify; From ba9210185c3ae122a9d83ef031fdbfdc69910a44 Mon Sep 17 00:00:00 2001 From: lincao Date: Thu, 24 Jul 2025 19:34:10 +0800 Subject: [PATCH 121/228] feat(chatbot): fix markdown rerender --- packages/pro-components/chat/chatbot/_example/basic.tsx | 2 +- .../pro-components/chat/chatbot/_example/hookComponent.tsx | 1 + packages/pro-components/chat/chatbot/_example/nostream.tsx | 2 +- packages/tdesign-react-aigc/package.json | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/basic.tsx b/packages/pro-components/chat/chatbot/_example/basic.tsx index 1d59738b10..ffc99eb2ae 100644 --- a/packages/pro-components/chat/chatbot/_example/basic.tsx +++ b/packages/pro-components/chat/chatbot/_example/basic.tsx @@ -105,7 +105,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: `http://localhost:3000/sse/normal`, + endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index f9c3dcf60b..d0ba3bf583 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -164,6 +164,7 @@ export default function ComponentsBuild() { const { value } = e.detail; const params = { prompt: value, + abc: 1, }; await sendUserMessage(params); setInputValue(''); diff --git a/packages/pro-components/chat/chatbot/_example/nostream.tsx b/packages/pro-components/chat/chatbot/_example/nostream.tsx index 87e6098e8b..f2e37a26c6 100644 --- a/packages/pro-components/chat/chatbot/_example/nostream.tsx +++ b/packages/pro-components/chat/chatbot/_example/nostream.tsx @@ -57,7 +57,7 @@ export default function chatSample() { // 聊天服务配置 const chatServiceConfig: ChatServiceConfig = { // 对话服务地址 - endpoint: `http://localhost:3000/fetch/normal`, + endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/fetch/normal`, stream: false, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (isAborted, req, result) => { diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 6464a46243..921dc659ce 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -1,6 +1,6 @@ { "name": "@tdesign-react/aigc", - "version": "0.1.0-alpha.13", + "version": "0.1.0-alpha.14", "title": "@tdesign-react/aigc", "description": "TDesign Pro Component for AIGC", "module": "es/index.js", @@ -50,7 +50,7 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.1.7", + "tdesign-web-components": "1.1.9", "classnames": "~2.5.1", "lodash-es": "^4.17.21" }, @@ -61,4 +61,4 @@ "tvision-charts-react": "^3.3.12", "express": "^4.17.3" } -} +} \ No newline at end of file From a6b4647abded71815dbe05d72b94941746c09c1d Mon Sep 17 00:00:00 2001 From: lincao Date: Thu, 24 Jul 2025 19:46:46 +0800 Subject: [PATCH 122/228] feat(chatbot): request params --- .../chat/chatbot/_example/hookComponent.tsx | 28 +++--- .../pro-components/chat/chatbot/chatbot.md | 93 +++++-------------- .../pro-components/chat/chatbot/core/index.ts | 5 +- packages/tdesign-react-aigc/package.json | 3 +- 4 files changed, 39 insertions(+), 90 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 922dafb7a9..9faf76fff8 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -13,10 +13,10 @@ import { TdChatSenderApi, ChatActionBar, isAIMessage, - useChat, getMessageContentForCopy, TdChatSenderParams, } from '@tdesign-react/aigc'; +import { useChat } from '../useChat'; import mockData from './mock/data'; export default function ComponentsBuild() { @@ -73,20 +73,17 @@ export default function ComponentsBuild() { } }, // 自定义请求参数 - onRequest: (innerParams: ChatRequestParams) => { - const { prompt } = innerParams; - return { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify({ - uid: 'abcd', - prompt, - think: true, - search: true, - }), - }; - }, + onRequest: (innerParams: ChatRequestParams) => ({ + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'abcd', + think: true, + search: true, + ...innerParams, + }), + }), }, }); @@ -164,6 +161,7 @@ export default function ComponentsBuild() { const { value } = e.detail; const params = { prompt: value, + abc: 1, }; await sendUserMessage(params); setInputValue(''); diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index 8d69320ae5..de88bb1380 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -7,62 +7,11 @@ spline: navigation ## 基本用法 -### 标准化集成 - -组件内置状态管理,SSE 解析,自动处理消息内容渲染与交互逻辑,可开箱即用快速集成实现标准聊天界面。本示例演示了如何快速创建一个具备以下功能的智能对话组件: - -- 初始化预设消息 -- 预设消息内容渲染支持(markdown、搜索、思考、建议等) -- 与服务端的 SSE(Server-Sent Events)通信,支持流式消息响应 -- 自定义流式内容结构解析 -- 自定义请求参数处理 -- 常用消息操作处理及回调(复制、重试、点赞/点踩) -- 支持手动触发填入 prompt, 重新生成,发送消息等 - -{{ basic }} - ### 组合式用法 可以通过 `useChat` Hook 提供的对话引擎实例及状态控制方法,同时自行组合拼装`ChatList`,`ChatMessage`, `ChatSender`等组件集成聊天界面,适合需要深度定制组件结构和消息处理流程的场景 {{ hookComponent }} -## 自定义 - -如果组件内置的消息渲染方案不能满足需求,还可以通过自定义**消息结构解析逻辑**和**消息内容渲染组件**来实现更多渲染需求。以下示例给出了一个自定义实现图表渲染的示例,实现自定义渲染需要完成**四步**,概括起来就是:**扩展类型,准备组件,解析数据,植入插槽**: - -- 1、扩展自定义消息体 type 类型 -- 2、实现自定义渲染的组件,示例中使用了 tvision-charts-react 实现图表渲染 -- 3、流式数据增量更新回调`onMessage`中可以对返回数据进行标准化解构,返回渲染组件所需的数据结构,同时可以通过返回`strategy`来决定**同类新增内容块**的追加策略(merge/append),如果需要更灵活影响到数据整合可以返回完整消息数组`AIMessageContent[]`,或者注册合并策略方法(参考下方‘任务规划’示例) -- 4、在 render 函数中遍历消息内容数组,植入自定义消息体渲染插槽,需保证 slot 名在 list 中的唯一性 - -如果组件内置的几种操作 `TdChatMessageActionName` 不能满足需求,示例中同时给出了**自定义消息操作区**的方法,可以自行实现更多操作。 - -{{ custom }} - -## 场景化示例 - -以下再通过几个常见的业务场景,展示下如何使用 `Chatbot` 组件 - -### 代码助手 - -通过使用 tdesign 开发登录框组件的案例,演示了使用 Chatbot 搭建简单的代码助手场景,该示例你可以了解到如何按需开启**markdown 渲染代码块**,如何**自定义实现代码预览** -{{ code }} - -### 文案助手 - -以下案例演示了使用 Chatbot 搭建简单的文案写作助手应用,通过该示例你可以了解到如何**发送附件**,同时演示了**附件类型的内容渲染** -{{ docs }} - -### 图像生成 - -以下案例演示了使用 Chatbot 搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** -{{ image }} - -### 任务规划 - -以下案例模拟了使用 Chatbot 搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,**自定义消息插槽名规则**,同时演示了**自定义任务流程渲染** -{{ agent }} - ### AGUI 协议支持 {{ agui }} @@ -71,16 +20,16 @@ spline: navigation ### Chatbot Props -名称 | 类型 | 默认值 | 说明 | 必传 --- | -- | -- | -- | -- -defaultMessages | Array | - | 初始消息数据列表。TS类型:`ChatMessagesData[]`。[详细类型定义](/react-aigc/components/chat-message?tab=api) | N -messageProps | Object/Function | - | 消息项配置。按角色聚合了消息项的配置透传`ChatMessage`组件,TS类型:`TdChatMessageConfig \| ((msg: ChatMessagesData) => Omit)` ,[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/blob/develop/src/chatbot/type.ts#L151) | N -listProps | Object | - | 消息列表配置。TS类型:`TdChatListProps`。 | N -senderProps | Object | - | 发送框配置,透传`ChatSender`组件。TS类型:`TdChatSenderProps`。[类型定义](./chat-sender?tab=api) | N -chatServiceConfig | Object | - | 聊天服务配置,见下方详细说明,TS类型:`ChatServiceConfig` | N -onMessageChange | Function | - | 消息列表数据变化回调,TS类型:`(e: CustomEvent) => void` | N -onChatReady | Function | - | 内部消息引擎初始化完成回调,TS类型:`(e: CustomEvent) => void` | N -onChatAfterSend | Function | - | 发送消息回调,TS类型:`(e: CustomEvent) => void` | N +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| ----------------- | --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | +| defaultMessages | Array | - | 初始消息数据列表。TS 类型:`ChatMessagesData[]`。[详细类型定义](/react-aigc/components/chat-message?tab=api) | N | +| messageProps | Object/Function | - | 消息项配置。按角色聚合了消息项的配置透传`ChatMessage`组件,TS 类型:`TdChatMessageConfig \| ((msg: ChatMessagesData) => Omit)` ,[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/blob/develop/src/chatbot/type.ts#L151) | N | +| listProps | Object | - | 消息列表配置。TS 类型:`TdChatListProps`。 | N | +| senderProps | Object | - | 发送框配置,透传`ChatSender`组件。TS 类型:`TdChatSenderProps`。[类型定义](./chat-sender?tab=api) | N | +| chatServiceConfig | Object | - | 聊天服务配置,见下方详细说明,TS 类型:`ChatServiceConfig` | N | +| onMessageChange | Function | - | 消息列表数据变化回调,TS 类型:`(e: CustomEvent) => void` | N | +| onChatReady | Function | - | 内部消息引擎初始化完成回调,TS 类型:`(e: CustomEvent) => void` | N | +| onChatAfterSend | Function | - | 发送消息回调,TS 类型:`(e: CustomEvent) => void` | N | ### TdChatListProps 消息列表配置 @@ -94,17 +43,17 @@ onChatAfterSend | Function | - | 发送消息回调,TS类型:`(e: CustomEven 聊天服务核心配置类型,主要作用包括基础通信配置,请求流程控制及全生命周期管理(初始化 → 传输 → 完成/中止),流式数据的分块处理策略,状态通知回调等。 -名称 | 类型 | 默认值 | 说明 | 必传 --- | -- | -- | -- | -- -endpoint | String | - | 聊天服务请求地址url | N -protocol | String | 'default' | 聊天服务协议,支持'default'和'agui' | N -stream | Boolean | true | 是否使用流式传输 | N -onStart | Function | - | 流开始传输时的回调。TS类型:`(params: ChatRequestParams) => RequestInit` | N -onRequest | Function | - | 请求前的回调,可修改请求参数。TS类型:`(params: ChatRequestParams) => RequestInit` | N -onMessage | Function | - | 处理流式消息的回调。TS类型:`(chunk: SSEChunkData) => AIMessageContent / AIMessageContent[] / null` | N -onComplete | Function | - | 请求结束时的回调。TS类型:`(isAborted: boolean, params: RequestInit, result?: any) => AIMessageContent / AIMessageContent[] / null` | N -onAbort | Function | - | 中止请求时的回调。TS类型:`() => Promise` | N -onError | Function | - | 错误处理回调。TS类型:`(err: Error \| Response) => void` | N +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| ---------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---- | +| endpoint | String | - | 聊天服务请求地址 url | N | +| protocol | String | 'default' | 聊天服务协议,支持'default'和'agui' | N | +| stream | Boolean | true | 是否使用流式传输 | N | +| onStart | Function | - | 流开始传输时的回调。TS 类型:`(params: ChatRequestParams) => RequestInit` | N | +| onRequest | Function | - | 请求前的回调,可修改请求参数。TS 类型:`(params: ChatRequestParams) => RequestInit` | N | +| onMessage | Function | - | 处理流式消息的回调。TS 类型:`(chunk: SSEChunkData) => AIMessageContent / AIMessageContent[] / null` | N | +| onComplete | Function | - | 请求结束时的回调。TS 类型:`(isAborted: boolean, params: RequestInit, result?: any) => AIMessageContent / AIMessageContent[] / null` | N | +| onAbort | Function | - | 中止请求时的回调。TS 类型:`() => Promise` | N | +| onError | Function | - | 错误处理回调。TS 类型:`(err: Error \| Response) => void` | N | ### Chatbot 实例方法 diff --git a/packages/pro-components/chat/chatbot/core/index.ts b/packages/pro-components/chat/chatbot/core/index.ts index ba39e536ff..e11a72b322 100644 --- a/packages/pro-components/chat/chatbot/core/index.ts +++ b/packages/pro-components/chat/chatbot/core/index.ts @@ -16,7 +16,6 @@ import type { } from './type'; import { isAIMessage } from './utils'; import { EventType } from './adapters/agui/events'; -import { handleError } from '../../../../../../tdesign-web-components/src/_common/js/upload/main'; export interface IChatEngine { init(config?: any, messages?: ChatMessagesData[]): void; @@ -61,11 +60,12 @@ export default class ChatEngine implements IChatEngine { } public async sendUserMessage(requestParams: ChatRequestParams) { - const { prompt, attachments } = requestParams; + const { prompt, attachments, ...rest } = requestParams; const userMessage = this.processor.createUserMessage(prompt, attachments); const aiMessage = this.processor.createAssistantMessage(); this.messageStore.createMultiMessages([userMessage, aiMessage]); const params = { + ...rest, prompt, attachments, messageID: aiMessage.id, @@ -199,6 +199,7 @@ export default class ChatEngine implements IChatEngine { } private async handleStreamRequest(params: ChatRequestParams) { + console.log('===params', params); const id = params.messageID; const isAGUI = this.config.protocol === 'agui'; this.setMessageStatus(id, 'streaming'); // todo: 这里应该在建立连接后在streaming diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 68e30a1370..6b201692ec 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -50,7 +50,8 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.1.7", + "tdesign-web-components": "1.1.9", + "zod": "^3.23.8", "classnames": "~2.5.1", "lodash-es": "^4.17.21" }, From 2876257c75f973fd8816193b93f73d5a42349f92 Mon Sep 17 00:00:00 2001 From: lincao Date: Thu, 24 Jul 2025 20:13:15 +0800 Subject: [PATCH 123/228] feat(chatbot): demo refine --- .../chat/chatbot/_example/hookComponent.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index d0ba3bf583..e398a4c39c 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -73,20 +73,17 @@ export default function ComponentsBuild() { } }, // 自定义请求参数 - onRequest: (innerParams: ChatRequestParams) => { - const { prompt } = innerParams; - return { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify({ - uid: 'abcd', - prompt, - think: true, - search: true, - }), - }; - }, + onRequest: (innerParams: ChatRequestParams) => ({ + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ + uid: 'abcd', + think: true, + search: true, + ...innerParams, + }), + }), }, }); From 20553befd10c0754e06d82e01093909481817ceb Mon Sep 17 00:00:00 2001 From: lincao Date: Thu, 24 Jul 2025 21:20:47 +0800 Subject: [PATCH 124/228] feat(chatbot): add demo --- .../pro-components/chat/_util/reactify.tsx | 5 +- .../chat/chatbot/_example/agui.tsx | 1 - .../chat/chatbot/_example/hookComponent.tsx | 113 ++++++++++++++---- .../pro-components/chat/chatbot/core/index.ts | 1 - 4 files changed, 94 insertions(+), 26 deletions(-) diff --git a/packages/pro-components/chat/_util/reactify.tsx b/packages/pro-components/chat/_util/reactify.tsx index 3665841190..3e411fd3ef 100644 --- a/packages/pro-components/chat/_util/reactify.tsx +++ b/packages/pro-components/chat/_util/reactify.tsx @@ -1,6 +1,5 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -const { Component, createRef, createElement, forwardRef } = React; +import React, { Component, createRef, createElement, forwardRef } from 'react'; +import ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client'; // 检测 React 版本 diff --git a/packages/pro-components/chat/chatbot/_example/agui.tsx b/packages/pro-components/chat/chatbot/_example/agui.tsx index c17657b7a8..e82ed6a479 100644 --- a/packages/pro-components/chat/chatbot/_example/agui.tsx +++ b/packages/pro-components/chat/chatbot/_example/agui.tsx @@ -13,7 +13,6 @@ import { useChat, } from '@tdesign-react/aigc'; import { getMessageContentForCopy, TdChatActionsName, TdChatSenderParams } from 'tdesign-web-components'; -import mockData from './mock/data'; export default function ComponentsBuild() { const listRef = useRef(null); diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 9faf76fff8..3e9985196e 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -18,6 +18,7 @@ import { } from '@tdesign-react/aigc'; import { useChat } from '../useChat'; import mockData from './mock/data'; +import { EventType } from '../core/adapters/agui/events'; export default function ComponentsBuild() { const listRef = useRef(null); @@ -28,7 +29,7 @@ export default function ComponentsBuild() { // 聊天服务配置 chatServiceConfig: { // 对话服务地址f - endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal`, + endpoint: `http://127.0.0.1:3000/sse/agui`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { @@ -43,33 +44,103 @@ export default function ComponentsBuild() { console.log('中断'); }, onMessage: (chunk: SSEChunkData): AIMessageContent => { - const { type, ...rest } = chunk.data; + const { type, toolCallId, toolCallName, delta, title } = chunk.data; + // switch (type) { + // case 'search': + // // 搜索 + // return { + // type: 'search', + // data: { + // title: rest.title || `搜索到${rest?.docs.length}条内容`, + // references: rest?.content, + // }, + // }; + // // 思考 + // case 'think': + // return { + // type: 'thinking', + // status: (status) => (/耗时/.test(rest?.title) ? 'complete' : status), + // data: { + // title: rest.title || '深度思考中', + // text: rest.content || '', + // }, + // }; + // // 正文 + // case 'text': + // return { + // type: 'markdown', + // data: rest?.msg || '', + // }; switch (type) { - case 'search': - // 搜索 + case 'TEXT_MESSAGE_START': return { - type: 'search', - data: { - title: rest.title || `搜索到${rest?.docs.length}条内容`, - references: rest?.content, - }, + type: 'markdown', + status: 'streaming', + data: '', + strategy: 'append', }; - // 思考 - case 'think': + case 'TEXT_MESSAGE_CHUNK': + case 'TEXT_MESSAGE_END': return { - type: 'thinking', - status: (status) => (/耗时/.test(rest?.title) ? 'complete' : status), - data: { - title: rest.title || '深度思考中', - text: rest.content || '', - }, + type: 'markdown', + status: type === 'TEXT_MESSAGE_END' ? 'complete' : 'streaming', + data: delta || '', + strategy: 'merge', }; - // 正文 - case 'text': + case EventType.THINKING_START: return { - type: 'markdown', - data: rest?.msg || '', + type: 'thinking', + data: { title: title || '思考中...' }, + status: 'streaming', + strategy: 'append', }; + case EventType.THINKING_TEXT_MESSAGE_CONTENT: + return { type: 'thinking', data: { text: delta }, status: 'streaming', strategy: 'merge' }; + case EventType.THINKING_END: + return { type: 'thinking', data: { title: title || '思考结束' }, status: 'complete' }; + + case EventType.TOOL_CALL_START: + case EventType.TOOL_CALL_ARGS: + this.toolCallMap[toolCallId] = { + name: toolCallName, + arguments: type === 'TOOL_CALL_ARGS' ? delta : '', + }; + if (toolCallName === 'search') { + return { + type: 'search', + data: { + title: '联网搜索中', + references: [], + }, + status: 'pending', + }; + } + return null; + case EventType.TOOL_CALL_CHUNK: + case EventType.TOOL_CALL_RESULT: + if (toolCallName === 'search') { + let parsed = { + title: '搜索中', + references: [], + }; + try { + parsed = JSON.parse(delta || content); + } catch {} + return { + type: 'search', + data: parsed, + status: type === 'TOOL_CALL_RESULT' ? 'complete' : 'streaming', + }; + } + return null; + case EventType.RUN_ERROR: + return [ + { + type: 'text', + data: 'Unknown error', + status: 'error', + }, + ]; } }, // 自定义请求参数 diff --git a/packages/pro-components/chat/chatbot/core/index.ts b/packages/pro-components/chat/chatbot/core/index.ts index e11a72b322..c64f60583a 100644 --- a/packages/pro-components/chat/chatbot/core/index.ts +++ b/packages/pro-components/chat/chatbot/core/index.ts @@ -199,7 +199,6 @@ export default class ChatEngine implements IChatEngine { } private async handleStreamRequest(params: ChatRequestParams) { - console.log('===params', params); const id = params.messageID; const isAGUI = this.config.protocol === 'agui'; this.setMessageStatus(id, 'streaming'); // todo: 这里应该在建立连接后在streaming From a94de9be995ff4c01fb744f6bfb2ea5fab20d275 Mon Sep 17 00:00:00 2001 From: lincao Date: Thu, 24 Jul 2025 21:28:35 +0800 Subject: [PATCH 125/228] feat(chatbot): multi sametype content render --- .../chat/chatbot/_example/hookComponent.tsx | 113 ++++-------------- packages/tdesign-react-aigc/package.json | 2 +- 2 files changed, 22 insertions(+), 93 deletions(-) diff --git a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx index 3e9985196e..9faf76fff8 100644 --- a/packages/pro-components/chat/chatbot/_example/hookComponent.tsx +++ b/packages/pro-components/chat/chatbot/_example/hookComponent.tsx @@ -18,7 +18,6 @@ import { } from '@tdesign-react/aigc'; import { useChat } from '../useChat'; import mockData from './mock/data'; -import { EventType } from '../core/adapters/agui/events'; export default function ComponentsBuild() { const listRef = useRef(null); @@ -29,7 +28,7 @@ export default function ComponentsBuild() { // 聊天服务配置 chatServiceConfig: { // 对话服务地址f - endpoint: `http://127.0.0.1:3000/sse/agui`, + endpoint: `https://1257786608-9i9j1kpa67.ap-guangzhou.tencentscf.com/sse/normal`, stream: true, // 流式对话结束(aborted为true时,表示用户主动结束对话,params为请求参数) onComplete: (aborted: boolean, params: RequestInit) => { @@ -44,103 +43,33 @@ export default function ComponentsBuild() { console.log('中断'); }, onMessage: (chunk: SSEChunkData): AIMessageContent => { - const { type, toolCallId, toolCallName, delta, title } = chunk.data; - // switch (type) { - // case 'search': - // // 搜索 - // return { - // type: 'search', - // data: { - // title: rest.title || `搜索到${rest?.docs.length}条内容`, - // references: rest?.content, - // }, - // }; - // // 思考 - // case 'think': - // return { - // type: 'thinking', - // status: (status) => (/耗时/.test(rest?.title) ? 'complete' : status), - // data: { - // title: rest.title || '深度思考中', - // text: rest.content || '', - // }, - // }; - // // 正文 - // case 'text': - // return { - // type: 'markdown', - // data: rest?.msg || '', - // }; + const { type, ...rest } = chunk.data; switch (type) { - case 'TEXT_MESSAGE_START': + case 'search': + // 搜索 return { - type: 'markdown', - status: 'streaming', - data: '', - strategy: 'append', - }; - case 'TEXT_MESSAGE_CHUNK': - case 'TEXT_MESSAGE_END': - return { - type: 'markdown', - status: type === 'TEXT_MESSAGE_END' ? 'complete' : 'streaming', - data: delta || '', - strategy: 'merge', + type: 'search', + data: { + title: rest.title || `搜索到${rest?.docs.length}条内容`, + references: rest?.content, + }, }; - case EventType.THINKING_START: + // 思考 + case 'think': return { type: 'thinking', - data: { title: title || '思考中...' }, - status: 'streaming', - strategy: 'append', + status: (status) => (/耗时/.test(rest?.title) ? 'complete' : status), + data: { + title: rest.title || '深度思考中', + text: rest.content || '', + }, }; - case EventType.THINKING_TEXT_MESSAGE_CONTENT: - return { type: 'thinking', data: { text: delta }, status: 'streaming', strategy: 'merge' }; - case EventType.THINKING_END: - return { type: 'thinking', data: { title: title || '思考结束' }, status: 'complete' }; - - case EventType.TOOL_CALL_START: - case EventType.TOOL_CALL_ARGS: - this.toolCallMap[toolCallId] = { - name: toolCallName, - arguments: type === 'TOOL_CALL_ARGS' ? delta : '', + // 正文 + case 'text': + return { + type: 'markdown', + data: rest?.msg || '', }; - if (toolCallName === 'search') { - return { - type: 'search', - data: { - title: '联网搜索中', - references: [], - }, - status: 'pending', - }; - } - return null; - case EventType.TOOL_CALL_CHUNK: - case EventType.TOOL_CALL_RESULT: - if (toolCallName === 'search') { - let parsed = { - title: '搜索中', - references: [], - }; - try { - parsed = JSON.parse(delta || content); - } catch {} - return { - type: 'search', - data: parsed, - status: type === 'TOOL_CALL_RESULT' ? 'complete' : 'streaming', - }; - } - return null; - case EventType.RUN_ERROR: - return [ - { - type: 'text', - data: 'Unknown error', - status: 'error', - }, - ]; } }, // 自定义请求参数 diff --git a/packages/tdesign-react-aigc/package.json b/packages/tdesign-react-aigc/package.json index 6b201692ec..f8c54a26f3 100644 --- a/packages/tdesign-react-aigc/package.json +++ b/packages/tdesign-react-aigc/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@babel/runtime": "~7.26.7", - "tdesign-web-components": "1.1.9", + "tdesign-web-components": "1.1.10", "zod": "^3.23.8", "classnames": "~2.5.1", "lodash-es": "^4.17.21" From ce374e8f54051077e77acdae02cefb94a71b37d4 Mon Sep 17 00:00:00 2001 From: carolin913 Date: Fri, 25 Jul 2025 14:59:41 +0800 Subject: [PATCH 126/228] feat(chatbot): add mock --- .../chat/_util/reactifyLazy.tsx | 95 + .../chat/chat-actionbar/index.ts | 13 - .../chat/chat-actionbar/index.tsx | 52 + .../chatbot/_example/README-travel-planner.md | 228 + .../chat/chatbot/_example/agent.tsx | 45 +- .../chat/chatbot/_example/agui.tsx | 6 +- .../chat/chatbot/_example/index.tsx | 101 + .../chat/chatbot/_example/mock/data.ts | 27 - .../chat/chatbot/_example/travel-planner.css | 270 + .../chat/chatbot/_example/travel.tsx | 529 ++ .../pro-components/chat/chatbot/chatbot.md | 58 +- .../core/adapters/AGUI_ADAPTER_README.md | 261 - .../chat/chatbot/core/adapters/README.md | 454 -- .../chatbot/core/adapters/agui-adapter.ts | 561 -- .../core/adapters/agui/agui-event-mapper.ts | 33 +- .../core/adapters/agui/agui-protocol.txt | 6406 +++++++++++++++++ .../chat/chatbot/core/adapters/agui/events.ts | 4 +- .../adapters/agui/travel-planner-mapper.ts | 237 + .../core/enhanced-server/llm-service.ts | 2 + .../core/enhanced-server/sse-client.ts | 2 + .../pro-components/chat/chatbot/core/index.ts | 23 +- .../chat/chatbot/core/processor/registry.ts | 45 - .../chat/chatbot/core/server/batch-client.ts | 63 + .../chatbot/core/server/connection-manager.ts | 88 + .../chat/chatbot/core/server/errors.ts | 50 + .../chat/chatbot/core/server/index.ts | 22 + .../chat/chatbot/core/server/llm-service.ts | 140 + .../chat/chatbot/core/server/sse-client.ts | 295 + .../chat/chatbot/core/server/sse-parser.ts | 149 + .../chat/chatbot/core/server/types.ts | 78 + .../pro-components/chat/chatbot/core/type.ts | 2 +- packages/tdesign-react-aigc/package.json | 5 +- 32 files changed, 8869 insertions(+), 1475 deletions(-) create mode 100644 packages/pro-components/chat/_util/reactifyLazy.tsx delete mode 100644 packages/pro-components/chat/chat-actionbar/index.ts create mode 100644 packages/pro-components/chat/chat-actionbar/index.tsx create mode 100644 packages/pro-components/chat/chatbot/_example/README-travel-planner.md create mode 100644 packages/pro-components/chat/chatbot/_example/index.tsx delete mode 100644 packages/pro-components/chat/chatbot/_example/mock/data.ts create mode 100644 packages/pro-components/chat/chatbot/_example/travel-planner.css create mode 100644 packages/pro-components/chat/chatbot/_example/travel.tsx delete mode 100644 packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md delete mode 100644 packages/pro-components/chat/chatbot/core/adapters/README.md delete mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui/agui-protocol.txt create mode 100644 packages/pro-components/chat/chatbot/core/adapters/agui/travel-planner-mapper.ts delete mode 100644 packages/pro-components/chat/chatbot/core/processor/registry.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/batch-client.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/connection-manager.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/errors.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/index.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/llm-service.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/sse-client.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/sse-parser.ts create mode 100644 packages/pro-components/chat/chatbot/core/server/types.ts diff --git a/packages/pro-components/chat/_util/reactifyLazy.tsx b/packages/pro-components/chat/_util/reactifyLazy.tsx new file mode 100644 index 0000000000..a4fa83518a --- /dev/null +++ b/packages/pro-components/chat/_util/reactifyLazy.tsx @@ -0,0 +1,95 @@ +import React, { forwardRef, useState, useEffect } from 'react'; +import reactify from './reactify'; + +type AnyProps = Record; + +// 组件注册表 +const componentRegistry = new Map>(); + +/** + * 注册Web组件 + * @param tagName 组件标签名 + * @param importPath 导入路径 + */ +export function registerWebComponent(tagName: string, importPath: string): Promise { + if (!componentRegistry.has(tagName)) { + componentRegistry.set( + tagName, + import(importPath) + .then((module) => { + if (module.default && typeof module.default === 'function') { + module.default(); // 调用注册函数 + } + }) + .catch((error) => { + console.error(`Failed to load component ${tagName}:`, error); + throw error; + }), + ); + } + return componentRegistry.get(tagName)!; +} + +/** + * 将Web组件转换为React组件(带懒加载) + * @param tagName 组件的自定义元素标签名 + * @param importPath 导入Web组件注册模块的路径 + * @param fallback 加载过程中显示的React节点 + */ +export const reactifyLazy = ( + tagName: string, + importPath: string, + fallback?: React.ReactNode, +) => { + // 使用reactify创建基础React组件 + const ReactComponent = reactify(tagName); + + return forwardRef((props, ref) => { + const [isLoaded, setIsLoaded] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + + registerWebComponent(tagName, importPath) + .then(() => { + if (isMounted) setIsLoaded(true); + }) + .catch((err) => { + if (isMounted) setError(err); + }); + + return () => { + isMounted = false; + }; + }, [tagName, importPath]); + + // 错误处理 + if (error) { + console.error(`Failed to load component ${tagName}`, error); + return fallback; + } + + // 加载状态 + if (!isLoaded) { + return fallback; + } + + // 渲染组件 + return ; + }); +}; + +// import { reactifyLazy } from './_util/reactifyLazy'; + +// const TButton = reactifyLazy<{ +// size: 'small' | 'medium' | 'large', +// variant: 'primary' | 'secondary' | 'outline' +// }>( +// 't-button', +// 'tdesign-web-components/esm/button', +//
+//
+// Loading button... +//
+// ); diff --git a/packages/pro-components/chat/chat-actionbar/index.ts b/packages/pro-components/chat/chat-actionbar/index.ts deleted file mode 100644 index 2ecadc6a60..0000000000 --- a/packages/pro-components/chat/chat-actionbar/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TdChatActionProps } from 'tdesign-web-components'; -import 'tdesign-web-components/lib/chat-action'; -import reactify from '../_util/reactify'; - -export const ChatActionBar: React.ForwardRefExoticComponent< - Omit & - React.RefAttributes & { - [key: string]: any; - } -> = reactify('t-chat-action'); - -export default ChatActionBar; -export type { TdChatActionProps } from 'tdesign-web-components'; diff --git a/packages/pro-components/chat/chat-actionbar/index.tsx b/packages/pro-components/chat/chat-actionbar/index.tsx new file mode 100644 index 0000000000..4fcaca3810 --- /dev/null +++ b/packages/pro-components/chat/chat-actionbar/index.tsx @@ -0,0 +1,52 @@ +import { TdChatActionProps } from 'tdesign-web-components'; +import 'tdesign-web-components/lib/chat-action'; +import reactify from '../_util/reactify'; + +export const ChatActionBar: React.ForwardRefExoticComponent< + Omit & + React.RefAttributes & { + [key: string]: any; + } +> = reactify('t-chat-action'); + +export default ChatActionBar; +export type { TdChatActionProps } from 'tdesign-web-components'; + +// 方案1 +// import { reactifyLazy } from './_util/reactifyLazy'; +// const ChatActionBar = reactifyLazy<{ +// size: 'small' | 'medium' | 'large', +// variant: 'primary' | 'secondary' | 'outline' +// }>( +// 't-chat-action', +// 'tdesign-web-components/esm/chat-action' +// ); + +// import ChatAction from 'tdesign-web-components/esm/chat-action'; +// import React, { forwardRef, useEffect } from 'react'; + +// // 注册Web Components组件 +// const registerChatAction = () => { +// if (!customElements.get('t-chat-action')) { +// customElements.define('t-chat-action', ChatAction); +// } +// }; + +// // 在组件挂载时注册 +// const useRegisterWebComponent = () => { +// useEffect(() => { +// registerChatAction(); +// }, []); +// }; + +// // 使用reactify创建React组件 +// const BaseChatActionBar = reactify('t-chat-action'); + +// // 包装组件,确保Web Components已注册 +// export const ChatActionBar2 = forwardRef< +// HTMLElement | undefined, +// Omit & { [key: string]: any } +// >((props, ref) => { +// useRegisterWebComponent(); +// return ; +// }); diff --git a/packages/pro-components/chat/chatbot/_example/README-travel-planner.md b/packages/pro-components/chat/chatbot/_example/README-travel-planner.md new file mode 100644 index 0000000000..5c6564fbe6 --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/README-travel-planner.md @@ -0,0 +1,228 @@ +# 旅游规划聊天应用 + +基于AG-UI协议的智能旅游规划聊天应用,展示了如何使用TDesign React聊天组件实现复杂的Agent对话场景。 + +## 功能特性 + +- 🌤️ **天气查询**: 自动获取目的地天气信息 +- 📅 **行程规划**: 智能规划每日旅游路线 +- 🏨 **酒店推荐**: 推荐合适的住宿选择 +- 📊 **进度跟踪**: 实时显示规划进度 +- 💬 **流式对话**: 支持AG-UI协议的流式响应 +- 🎨 **丰富展示**: 卡片式展示天气、行程、酒店信息 + +## 文件结构 + +``` +travel-planner/ +├── travel-planner.tsx # 主要组件文件 +├── travel-planner.css # 样式文件 +├── mock/ +│ └── agui-server.js # 模拟AG-UI协议服务器 +└── README-travel-planner.md # 说明文档 +``` + +## 快速开始 + +### 1. 启动模拟服务器 + +```bash +cd packages/pro-components/chat/chatbot/_example/mock +node agui-server.js +``` + +服务器将在 `http://localhost:3000` 启动。 + +### 2. 运行示例 + +在TDesign React项目中运行旅游规划示例: + +```bash +npm run dev +``` + +访问示例页面,选择 "Travel Planner" 示例。 + +## AG-UI协议适配 + +### 支持的事件类型 + +- `RUN_STARTED` - 运行开始 +- `STEP_STARTED` - 步骤开始 +- `STEP_FINISHED` - 步骤完成 +- `THINKING_TEXT_MESSAGE_CONTENT` - 思考过程 +- `TOOL_CALL_RESULT` - 工具调用结果 +- `TEXT_MESSAGE_CHUNK` - 文本消息块 +- `RUN_FINISHED` - 运行完成 + +### 自定义消息类型 + +```typescript +declare module '@tdesign-react/aigc' { + interface AIContentTypeOverrides { + weather: ChatBaseContent<'weather', { weather: any[] }>; + itinerary: ChatBaseContent<'itinerary', { plan: any[] }>; + hotel: ChatBaseContent<'hotel', { hotels: any[] }>; + step_progress: ChatBaseContent<'step_progress', { steps: any[] }>; + thinking: ChatBaseContent<'thinking', { title: string; content: string }>; + } +} +``` + +## 组件说明 + +### WeatherCard +显示天气预报信息的卡片组件。 + +### ItineraryCard +展示行程规划的时间线组件。 + +### HotelCard +显示酒店推荐信息的列表组件。 + +### StepProgress +显示规划进度的侧边栏组件。 + +## 配置说明 + +### 聊天服务配置 + +```typescript +chatServiceConfig: { + endpoint: 'http://127.0.0.1:3000/sse/agui', + protocol: 'agui', + stream: true, + onMessage: (chunk): AIMessageContent => { + // AG-UI协议消息处理逻辑 + }, + onRequest: (innerParams: ChatRequestParams) => { + // 自定义请求参数 + }, +} +``` + +### 消息处理流程 + +1. **接收AG-UI事件**: 通过SSE接收AG-UI协议事件 +2. **事件解析**: 根据事件类型进行相应处理 +3. **状态更新**: 更新步骤进度和UI状态 +4. **内容渲染**: 将数据转换为可视化组件 +5. **用户交互**: 处理用户操作和反馈 + +## 使用示例 + +### 基本用法 + +```typescript +import TravelPlannerChat from './travel-planner'; + +function App() { + return ( +
+ +
+ ); +} +``` + +### 自定义配置 + +```typescript +const customConfig = { + endpoint: 'https://your-agui-server.com/sse/agui', + agentType: 'custom-travel-planner', + defaultPrompt: '请为我规划一个上海3日游行程', +}; +``` + +## 开发说明 + +### 扩展新的消息类型 + +1. 在类型声明中添加新类型: + +```typescript +declare module '@tdesign-react/aigc' { + interface AIContentTypeOverrides { + restaurant: ChatBaseContent<'restaurant', { restaurants: any[] }>; + } +} +``` + +2. 创建对应的渲染组件: + +```typescript +const RestaurantCard = ({ restaurants }) => ( + + {/* 餐厅信息展示 */} + +); +``` + +3. 在消息处理中添加处理逻辑: + +```typescript +case 'TOOL_CALL_RESULT': + if (rest.toolCallName === 'get_restaurants') { + return { + type: 'restaurant', + data: { restaurants: JSON.parse(rest.content) }, + }; + } +``` + +### 自定义样式 + +修改 `travel-planner.css` 文件来自定义组件样式: + +```css +.travel-planner-container { + /* 自定义容器样式 */ +} + +.weather-card { + /* 自定义天气卡片样式 */ +} +``` + +## 技术架构 + +### 核心技术栈 + +- **React**: 前端框架 +- **TDesign React**: UI组件库 +- **AG-UI协议**: 智能Agent通信协议 +- **SSE**: 服务器推送事件 +- **TypeScript**: 类型安全 + +### 数据流 + +``` +用户输入 → 发送请求 → AG-UI服务器 → SSE事件流 → 事件解析 → 状态更新 → UI渲染 +``` + +## 常见问题 + +### Q: 如何修改默认的服务器地址? + +A: 在组件的 `chatServiceConfig.endpoint` 中修改服务器地址。 + +### Q: 如何添加新的步骤? + +A: 在 `stepProgress` 状态中添加新的步骤,并在事件处理中添加对应的 `STEP_STARTED` 和 `STEP_FINISHED` 处理。 + +### Q: 如何自定义卡片样式? + +A: 修改对应的CSS类,或者创建新的卡片组件。 + +## 贡献指南 + +1. Fork 项目 +2. 创建功能分支 +3. 提交更改 +4. 推送到分支 +5. 创建 Pull Request + +## 许可证 + +本项目基于 MIT 许可证开源。 diff --git a/packages/pro-components/chat/chatbot/_example/agent.tsx b/packages/pro-components/chat/chatbot/_example/agent.tsx index 18ea25cc3e..2cb8e8c0ef 100644 --- a/packages/pro-components/chat/chatbot/_example/agent.tsx +++ b/packages/pro-components/chat/chatbot/_example/agent.tsx @@ -40,29 +40,25 @@ const AgentTimeline = ({ steps }) => ( ); // 扩展自定义消息体类型 -declare module 'tdesign-react' { - interface AIContentTypeOverrides { - agent: ChatBaseContent< - 'agent', - { - id: string; - state: 'pending' | 'command' | 'result' | 'finish'; - content: { - steps?: { - step: string; - agent_id: string; - status: string; - tasks?: { - type: 'command' | 'result'; - text: string; - }[]; - }[]; - text?: string; - }; - } - >; +type AgentContent = ChatBaseContent< + 'agent', + { + id: string; + state: 'pending' | 'command' | 'result' | 'finish'; + content: { + steps?: { + step: string; + agent_id: string; + status: string; + tasks?: { + type: 'command' | 'result'; + text: string; + }[]; + }[]; + text?: string; + }; } -} +>; // 默认初始化消息 const mockData: ChatMessagesData[] = [ @@ -151,7 +147,8 @@ export default function ChatBotReact() { } // 此处增加自定义消息内容合并策略逻辑 // 该示例agent类型结构比较复杂,根据任务步骤的state有不同的策略,组件内onMessage这里提供了的strategy无法满足,可以通过注册合并策略自行实现 - chatRef.current.registerMergeStrategy('agent', (newChunk, existing) => { + chatRef.current.registerMergeStrategy('agent', (newChunk, existing) => { + console.log('newChunk, existing', newChunk, existing); // 创建新对象避免直接修改原状态 const updated = { ...existing, @@ -218,7 +215,7 @@ export default function ChatBotReact() { > {mockMessage ?.map((msg) => - msg.content.map((item, index) => { + msg?.content?.map((item, index) => { if (item.type === 'agent') { return (
diff --git a/packages/pro-components/chat/chatbot/_example/agui.tsx b/packages/pro-components/chat/chatbot/_example/agui.tsx index c17657b7a8..ddc1e39ea5 100644 --- a/packages/pro-components/chat/chatbot/_example/agui.tsx +++ b/packages/pro-components/chat/chatbot/_example/agui.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useMemo, useRef, useState } from 'react'; +import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { type TdChatMessageConfig, type ChatRequestParams, @@ -10,10 +10,9 @@ import { TdChatSenderApi, ChatActionBar, isAIMessage, - useChat, } from '@tdesign-react/aigc'; import { getMessageContentForCopy, TdChatActionsName, TdChatSenderParams } from 'tdesign-web-components'; -import mockData from './mock/data'; +import { useChat } from '../useChat'; export default function ComponentsBuild() { const listRef = useRef(null); @@ -136,6 +135,7 @@ export default function ComponentsBuild() { }; const stopHandler = () => { + console.log('stopHandler'); chatEngine.abortChat(); }; diff --git a/packages/pro-components/chat/chatbot/_example/index.tsx b/packages/pro-components/chat/chatbot/_example/index.tsx new file mode 100644 index 0000000000..8b9db5848f --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/index.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Space, Card } from 'tdesign-react'; +import BasicExample from './basic'; +import AgentExample from './agent'; +import AguiExample from './agui'; +import TravelPlannerExample from './travel-planner'; +import AguiStepExample from './agui-step'; + +const examples = [ + { + title: '基础聊天', + description: '基本的聊天功能演示', + component: BasicExample, + }, + { + title: 'Agent聊天', + description: '智能Agent对话演示', + component: AgentExample, + }, + { + title: 'AG-UI协议', + description: 'AG-UI协议基础演示', + component: AguiExample, + }, + { + title: 'AG-UI步骤演示', + description: 'AG-UI协议步骤化演示', + component: AguiStepExample, + }, + { + title: '🏖️ 旅游规划助手', + description: '基于AG-UI协议的智能旅游规划聊天应用', + component: TravelPlannerExample, + featured: true, + }, +]; + +export default function ChatbotExamples() { + const [activeExample, setActiveExample] = React.useState(4); // 默认显示旅游规划助手 + + const ActiveComponent = examples[activeExample].component; + + return ( +
+
+

TDesign React Chatbot 示例

+

展示基于AG-UI协议的智能聊天组件功能

+
+ +
+ {/* 侧边栏 */} +
+ + + {examples.map((example, index) => ( +
setActiveExample(index)} + style={{ + padding: '12px', + borderRadius: '6px', + cursor: 'pointer', + border: `1px solid ${activeExample === index ? '#0052d9' : '#e7e7e7'}`, + backgroundColor: activeExample === index ? '#f2f6ff' : 'white', + transition: 'all 0.2s', + }} + > +
+ {example.title} +
+
+ {example.description} +
+
+ ))} +
+
+
+ + {/* 主内容区 */} +
+ + + +
+
+
+ ); +} diff --git a/packages/pro-components/chat/chatbot/_example/mock/data.ts b/packages/pro-components/chat/chatbot/_example/mock/data.ts deleted file mode 100644 index 4b86b8cc20..0000000000 --- a/packages/pro-components/chat/chatbot/_example/mock/data.ts +++ /dev/null @@ -1,27 +0,0 @@ -export default { - normal: [ - { - id: '1', - role: 'user', - status: 'complete', - content: [ - { - type: 'text', - data: '牛顿第一定律是否适用于所有参考系?', - }, - ], - }, - { - id: '2', - role: 'assistant', - status: 'complete', - content: [ - { - type: 'text', - data: '牛顿第一定律并不适用于所有参考系,它只适用于惯性参考系。在质点不受外力作用时,能够判断出质点静止或作匀速直线运动的参考系一定是惯性参考系,因此只有在惯性参考系中牛顿第一定律才适用。', - }, - ], - }, - ], - image: [], -}; diff --git a/packages/pro-components/chat/chatbot/_example/travel-planner.css b/packages/pro-components/chat/chatbot/_example/travel-planner.css new file mode 100644 index 0000000000..f874ee4db5 --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/travel-planner.css @@ -0,0 +1,270 @@ +/* 主容器 */ +.travel-planner-container { + height: 600px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 聊天内容区域 */ +.chat-content { + flex: 1; + display: flex; + overflow: hidden; +} + +/* 主聊天区域 */ +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 右侧规划面板 */ +.planning-sidebar { + width:180px; + padding: 0 16px; + overflow-y: auto; +} + +/* 内容卡片通用样式 */ +.content-card { + margin: 12px 0; +} + +/* 天气卡片 */ +.weather-card { + border: 1px solid #e5e7eb; + border-radius: 8px; +} + +.weather-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: #1f2937; +} + +.weather-title { + font-size: 14px; +} + +.weather-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.weather-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: #f9fafb; + border-radius: 6px; +} + +.weather-item .day { + font-weight: 500; + color: #374151; +} + +.weather-item .condition { + color: #6b7280; + font-size: 14px; +} + +.weather-item .temp { + font-weight: 600; + color: #0052d9; +} + +/* 行程卡片 */ +.itinerary-card { + border: 1px solid #e5e7eb; + border-radius: 8px; +} + +.itinerary-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: #1f2937; +} + +.itinerary-title { + font-size: 14px; +} + +.day-activities { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} + +.activity-tag { + font-size: 12px; +} + +/* 酒店卡片 */ +.hotel-card { + border: 1px solid #e5e7eb; + border-radius: 8px; +} + +.hotel-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: #1f2937; +} + +.hotel-title { + font-size: 14px; +} + +.hotel-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.hotel-item { + padding: 12px; + background: #f9fafb; + border-radius: 6px; + border: 1px solid #e5e7eb; +} + +.hotel-info { + display: flex; + flex-direction: column; + gap: 8px; +} + +.hotel-name { + font-weight: 600; + color: #1f2937; + font-size: 14px; +} + +.hotel-details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.hotel-price { + font-weight: 600; + color: #dc2626; + font-size: 14px; +} + +/* 规划状态面板 */ +.planning-state-panel { + border: 1px solid #e5e7eb; + border-radius: 8px; +} + +.panel-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: #1f2937; +} + +.panel-title { + flex: 1; + font-size: 14px; +} + +.progress-steps { + margin: 12px 0; +} + +.step-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; +} + +.step-title { + font-size: 14px; + color: #374151; +} + +/* 最终摘要 */ +.final-summary { + margin-top: 12px; +} + +.summary-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; + font-weight: 600; + color: #1f2937; + font-size: 13px; +} + +.summary-content { + font-size: 12px; + color: #6b7280; + line-height: 1.5; +} + +.summary-content div { + margin: 4px 0; +} + +/* 步骤进度动画 */ +.step-item { + transition: all 0.3s ease; +} + +.step-item:hover { + background: rgba(0, 82, 217, 0.05); + border-radius: 6px; + padding: 8px; + margin: -8px; +} + +/* Loading动画 */ +.loading-spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .travel-planner-container { + flex-direction: column; + } + + .planning-sidebar { + width: 100%; + height: auto; + border-left: none; + border-top: 1px solid #e5e7eb; + } +} diff --git a/packages/pro-components/chat/chatbot/_example/travel.tsx b/packages/pro-components/chat/chatbot/_example/travel.tsx new file mode 100644 index 0000000000..cca309af30 --- /dev/null +++ b/packages/pro-components/chat/chatbot/_example/travel.tsx @@ -0,0 +1,529 @@ +import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import { + type TdChatMessageConfig, + type ChatRequestParams, + type ChatMessagesData, + type AIMessageContent, + type ChatBaseContent, + ChatList, + ChatSender, + ChatMessage, + TdChatListApi, + TdChatSenderApi, + ChatActionBar, + isAIMessage, + useChat, +} from '@tdesign-react/aigc'; +import { getMessageContentForCopy, TdChatActionsName, TdChatSenderParams } from 'tdesign-web-components'; +import { Card, Timeline, Tag, Divider } from 'tdesign-react'; +import { + CheckCircleFilledIcon, + LocationIcon, + CalendarIcon, + CloudIcon, + HomeIcon, + InfoCircleIcon, + LoadingIcon, + TimeIcon, +} from 'tdesign-icons-react'; + +import './travel-planner.css'; + +// 扩展自定义消息体类型 +declare module '@tdesign-react/aigc' { + interface AIContentTypeOverrides { + weather: ChatBaseContent<'weather', { weather: any[] }>; + itinerary: ChatBaseContent<'itinerary', { plan: any[] }>; + hotel: ChatBaseContent<'hotel', { hotels: any[] }>; + planning_state: ChatBaseContent<'planning_state', { state: any }>; + } +} + +// 天气组件 +const WeatherCard = ({ weather }: { weather: any[] }) => ( + +
+ + 未来5天天气预报 +
+
+ {weather.map((day, index) => ( +
+ 第{day.day}天 + {day.condition} + + {day.high}°/{day.low}° + +
+ ))} +
+
+); + +// 行程规划组件 +const ItineraryCard = ({ plan }: { plan: any[] }) => ( + +
+ + 行程安排 +
+ + {plan.map((dayPlan, index) => ( + } + > +
+ {dayPlan.activities.map((activity: string, actIndex: number) => ( + + {activity} + + ))} +
+
+ ))} +
+
+); + +// 酒店推荐组件 +const HotelCard = ({ hotels }: { hotels: any[] }) => ( + +
+ + 酒店推荐 +
+
+ {hotels.map((hotel, index) => ( +
+
+ {hotel.name} +
+ + 评分 {hotel.rating} + + ¥{hotel.price}/晚 +
+
+
+ ))} +
+
+); + +// 规划状态面板组件 +const PlanningStatePanel = ({ state, currentStep }: { state: any; currentStep?: string }) => { + if (!state) return null; + + const { itinerary, status } = state; + + // 定义步骤顺序和状态 + const allSteps = [ + { name: '天气查询', key: 'weather', completed: !!itinerary?.weather }, + { name: '行程规划', key: 'plan', completed: !!itinerary?.plan }, + { name: '酒店推荐', key: 'hotels', completed: !!itinerary?.hotels }, + ]; + + // 获取步骤状态 + const getStepStatus = (step: any) => { + if (step.completed) return 'completed'; + if ( + currentStep === step.name || + (status === 'weather_querying' && step.key === 'weather') || + (status === 'planning' && step.key === 'plan') || + (status === 'hotel_recommending' && step.key === 'hotels') + ) { + return 'running'; + } + return 'pending'; + }; + + // 获取步骤图标 + const getStepIcon = (step: any) => { + const stepStatus = getStepStatus(step); + + switch (stepStatus) { + case 'completed': + return ; + case 'running': + return ; + default: + return ; + } + }; + + // 获取步骤标签 + const getStepTag = (step: any) => { + const stepStatus = getStepStatus(step); + + switch (stepStatus) { + case 'completed': + return ( + + 已完成 + + ); + case 'running': + return ( + + 进行中 + + ); + default: + return ( + + 等待中 + + ); + } + }; + + return ( + +
+ + 规划进度 + + {status === 'finished' ? '已完成' : status === 'planning' ? '规划中' : '准备中'} + +
+ +
+ + {allSteps.map((step, index) => ( + +
+
{step.name}
+ {getStepTag(step)} +
+
+ ))} +
+
+ + {/* 显示最终结果摘要 */} + {status === 'finished' && itinerary && ( +
+ +
+ + 规划摘要 +
+
+ {itinerary.weather &&
• 天气信息: {itinerary.weather.length}天预报
} + {itinerary.plan &&
• 行程安排: {itinerary.plan.length}天计划
} + {itinerary.hotels &&
• 酒店推荐: {itinerary.hotels.length}个选择
} +
+
+ )} +
+ ); +}; + +export default function TravelPlannerChat() { + const listRef = useRef(null); + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState('请为我规划一个北京5日游行程'); + + // 规划状态管理 - 用于右侧面板展示 + const [planningState, setPlanningState] = useState(null); + const [currentStep, setCurrentStep] = useState(''); + + const { chatEngine, messages, status } = useChat({ + defaultMessages: [], + // 聊天服务配置 + chatServiceConfig: { + // 对话服务地址 - 使用现有的服务 + endpoint: `http://127.0.0.1:3000/sse/agui`, + protocol: 'agui', + stream: true, + // 流式对话结束 + onComplete: (aborted: boolean, params?: RequestInit) => { + console.log('旅游规划完成', aborted, params); + return null; + }, + // 流式对话过程中出错 + onError: (err: Error | Response) => { + console.error('旅游规划服务错误:', err); + }, + // 流式对话过程中用户主动结束对话 + onAbort: async () => { + console.log('用户取消旅游规划'); + }, + // AG-UI协议消息处理 - 优先级高于内置处理 + onMessage: (chunk): AIMessageContent => { + const { type, ...rest } = chunk.data; + + console.log(`AG-UI事件: ${type}`, rest); + + switch (type) { + // ========== 步骤开始/结束事件处理 ========== + case 'STEP_STARTED': + console.log('步骤开始:', rest.stepName); + setCurrentStep(rest.stepName); + break; + + case 'STEP_FINISHED': + console.log('步骤完成:', rest.stepName); + setCurrentStep(''); + break; + + // ========== 工具调用结果处理 ========== + case 'TOOL_CALL_RESULT': + // 根据工具类型返回不同的展示组件 + if (rest.toolCallName === 'get_weather_forecast') { + try { + const weatherData = JSON.parse(rest.content); + return { + type: 'weather', + data: { weather: weatherData }, + }; + } catch (e) { + console.error('解析天气数据失败:', e); + } + } + + if (rest.toolCallName === 'plan_itinerary') { + try { + const planData = JSON.parse(rest.content); + return { + type: 'itinerary', + data: { plan: planData }, + }; + } catch (e) { + console.error('解析行程数据失败:', e); + } + } + + if (rest.toolCallName === 'get_hotel_details') { + try { + const hotelData = JSON.parse(rest.content); + return { + type: 'hotel', + data: { hotels: hotelData }, + }; + } catch (e) { + console.error('解析酒店数据失败:', e); + } + } + break; + + // ========== 状态管理事件处理 ========== + case 'STATE_SNAPSHOT': + console.log('状态快照:', rest.snapshot); + setPlanningState(rest.snapshot); + return { + type: 'planning_state', + data: { state: rest.snapshot }, + }; + + case 'STATE_DELTA': + console.log('状态变更:', rest.delta); + // 应用状态变更到当前状态 + setPlanningState((prevState: any) => { + if (!prevState) return prevState; + + const newState = { ...prevState }; + rest.delta.forEach((change: any) => { + const { op, path, value } = change; + if (op === 'replace') { + // 简单的路径替换逻辑 + if (path === '/status') { + newState.status = value; + } + } else if (op === 'add') { + // 简单的路径添加逻辑 + if (path.startsWith('/itinerary/')) { + if (!newState.itinerary) newState.itinerary = {}; + const key = path.split('/').pop(); + newState.itinerary[key] = value; + } + } + }); + return newState; + }); + + // 返回更新后的状态组件 + return { + type: 'planning_state', + data: { state: planningState }, + }; + + // 其他事件类型让内置处理器处理 + default: + return null; // 返回null让内置处理器处理 + } + + return null; + }, + // 自定义请求参数 + onRequest: (innerParams: ChatRequestParams) => { + const { prompt } = innerParams; + return { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uid: 'travel_planner_uid', + prompt, + agentType: 'travel-planner', + }), + }; + }, + }, + }); + + const senderLoading = useMemo(() => status === 'pending' || status === 'streaming', [status]); + + // 消息属性配置 + const messageProps: TdChatMessageConfig = { + user: { + variant: 'base', + placement: 'right', + }, + assistant: { + placement: 'left', + chatContentProps: { + thinking: { + maxHeight: 120, + }, + }, + }, + }; + + const getChatActionBar = (isLast: boolean) => { + let filterActions = ['replay', 'good', 'bad', 'copy']; + if (!isLast) { + filterActions = filterActions.filter((item) => item !== 'replay'); + } + return filterActions; + }; + + const actionHandler = (name: string, data?: any) => { + switch (name) { + case 'replay': { + console.log('重新规划旅游行程'); + chatEngine.regenerateAIMessage(); + return; + } + case 'good': + console.log('用户满意此次规划'); + break; + case 'bad': + console.log('用户不满意此次规划'); + break; + default: + console.log('触发操作', name, 'data', data); + } + }; + + /** 渲染消息内容体 */ + const renderMsgContents = (message: ChatMessagesData, isLast: boolean): ReactNode => ( + <> + {message.content.map((item, index) => { + // 天气卡片 + if (item.type === 'weather') { + return ( +
+ +
+ ); + } + + // 行程规划卡片 + if (item.type === 'itinerary') { + return ( +
+ +
+ ); + } + + // 酒店推荐卡片 + if (item.type === 'hotel') { + return ( +
+ +
+ ); + } + + // 规划状态面板 - 不在消息中显示,只用于更新右侧面板 + if (item.type === 'planning_state') { + return null; + } + + return null; + })} + + {isAIMessage(message) && message.status === 'complete' ? ( + + ) : null} + + ); + + const sendUserMessage = async (requestParams: ChatRequestParams) => { + // 重置规划状态 + setPlanningState(null); + + await chatEngine.sendUserMessage(requestParams); + listRef.current?.scrollList({ to: 'bottom' }); + }; + + const inputChangeHandler = (e: CustomEvent) => { + setInputValue(e.detail); + }; + + const sendHandler = async (e: CustomEvent) => { + const { value } = e.detail; + const params = { + prompt: value, + }; + await sendUserMessage(params); + setInputValue(''); + }; + + const stopHandler = () => { + console.log('停止旅游规划'); + chatEngine.abortChat(); + }; + + return ( +
+
+
+ + {messages.map((message, idx) => ( + + {renderMsgContents(message, idx === messages.length - 1)} + + ))} + + + +
+ + {/* 右侧规划状态面板 */} +
+ +
+
+
+ ); +} diff --git a/packages/pro-components/chat/chatbot/chatbot.md b/packages/pro-components/chat/chatbot/chatbot.md index 8d69320ae5..067a5ddf04 100644 --- a/packages/pro-components/chat/chatbot/chatbot.md +++ b/packages/pro-components/chat/chatbot/chatbot.md @@ -7,66 +7,20 @@ spline: navigation ## 基本用法 -### 标准化集成 - -组件内置状态管理,SSE 解析,自动处理消息内容渲染与交互逻辑,可开箱即用快速集成实现标准聊天界面。本示例演示了如何快速创建一个具备以下功能的智能对话组件: - -- 初始化预设消息 -- 预设消息内容渲染支持(markdown、搜索、思考、建议等) -- 与服务端的 SSE(Server-Sent Events)通信,支持流式消息响应 -- 自定义流式内容结构解析 -- 自定义请求参数处理 -- 常用消息操作处理及回调(复制、重试、点赞/点踩) -- 支持手动触发填入 prompt, 重新生成,发送消息等 - -{{ basic }} - ### 组合式用法 可以通过 `useChat` Hook 提供的对话引擎实例及状态控制方法,同时自行组合拼装`ChatList`,`ChatMessage`, `ChatSender`等组件集成聊天界面,适合需要深度定制组件结构和消息处理流程的场景 -{{ hookComponent }} - -## 自定义 - -如果组件内置的消息渲染方案不能满足需求,还可以通过自定义**消息结构解析逻辑**和**消息内容渲染组件**来实现更多渲染需求。以下示例给出了一个自定义实现图表渲染的示例,实现自定义渲染需要完成**四步**,概括起来就是:**扩展类型,准备组件,解析数据,植入插槽**: - -- 1、扩展自定义消息体 type 类型 -- 2、实现自定义渲染的组件,示例中使用了 tvision-charts-react 实现图表渲染 -- 3、流式数据增量更新回调`onMessage`中可以对返回数据进行标准化解构,返回渲染组件所需的数据结构,同时可以通过返回`strategy`来决定**同类新增内容块**的追加策略(merge/append),如果需要更灵活影响到数据整合可以返回完整消息数组`AIMessageContent[]`,或者注册合并策略方法(参考下方‘任务规划’示例) -- 4、在 render 函数中遍历消息内容数组,植入自定义消息体渲染插槽,需保证 slot 名在 list 中的唯一性 - -如果组件内置的几种操作 `TdChatMessageActionName` 不能满足需求,示例中同时给出了**自定义消息操作区**的方法,可以自行实现更多操作。 - -{{ custom }} - -## 场景化示例 - -以下再通过几个常见的业务场景,展示下如何使用 `Chatbot` 组件 - -### 代码助手 - -通过使用 tdesign 开发登录框组件的案例,演示了使用 Chatbot 搭建简单的代码助手场景,该示例你可以了解到如何按需开启**markdown 渲染代码块**,如何**自定义实现代码预览** -{{ code }} - -### 文案助手 - -以下案例演示了使用 Chatbot 搭建简单的文案写作助手应用,通过该示例你可以了解到如何**发送附件**,同时演示了**附件类型的内容渲染** -{{ docs }} - -### 图像生成 - -以下案例演示了使用 Chatbot 搭建简单的图像生成应用,通过该示例你可以了解到如何**自定义输入框操作区域**,同时演示了**自定义生图内容渲染** -{{ image }} - -### 任务规划 - -以下案例模拟了使用 Chatbot 搭建任务规划型智能体应用,分步骤依次执行并输出结果,通过该示例你可以了解到如何**注册自定义消息内容合并策略**,**自定义消息插槽名规则**,同时演示了**自定义任务流程渲染** {{ agent }} -### AGUI 协议支持 +## AGUI 协议 +### 基本用法 {{ agui }} + +### 步骤规划 +{{ travel }} + ## API ### Chatbot Props diff --git a/packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md b/packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md deleted file mode 100644 index 4aba113872..0000000000 --- a/packages/pro-components/chat/chatbot/core/adapters/AGUI_ADAPTER_README.md +++ /dev/null @@ -1,261 +0,0 @@ -# AG-UI 协议适配器 - -TDesign Chatbot 组件的 AG-UI(Agent User Interaction Protocol)协议适配器,支持标准化AI代理通信。 - -## 概述 - -AG-UI 是一个用于前端应用与 AI 代理通信的标准化协议。本适配器使 TDesign Chatbot 组件能够: - -1. **兼容 AG-UI 标准**:与遵循 AG-UI 协议的服务端和前端工具无缝集成 -2. **向后兼容**:保持对现有 TDesign 格式的完全支持 -3. **灵活适配**:自动转换不同数据格式,提供可插拔的适配层 - -## 三种使用场景 - -### 场景 1:服务端原生 AG-UI 协议 - -**适用于**:服务端已经支持 AG-UI 协议的新项目 - -```typescript -import { createAGUIAdapter } from './adapters/agui-adapter'; - -const aguiConfig = { - mode: 'native' as const, - agentId: 'my-agent', - onEvent: (event) => { - console.log('AG-UI事件:', event); - } -}; - -const adapter = createAGUIAdapter(aguiConfig); -const config = adapter.wrapConfig({ - endpoint: '/api/chat/agui-native', - stream: true -}); - -const engine = new ChatEngine(); -engine.init(config); -``` - -**服务端响应格式**: -```json -{ - "type": "TEXT_MESSAGE_CHUNK", - "data": { - "content": "你好!", - "contentType": "text" - }, - "timestamp": 1234567890, - "runId": "run_abc", - "agentId": "my-agent" -} -``` - -### 场景 2:适配器转换模式 - -**适用于**:现有项目需要 AG-UI 标准化,但服务端暂未支持 - -```typescript -const aguiConfig = { - mode: 'adapter' as const, - agentId: 'my-agent', - onEvent: (event) => { - // 接收转换后的标准AG-UI事件 - console.log('转换后的AG-UI事件:', event); - }, - // 可选:自定义适配逻辑 - customAdapter: (chunk) => { - if (chunk.data?.type === 'special_format') { - return { - type: 'CUSTOM', - data: { type: 'special', content: chunk.data.content } - }; - } - return null; // 使用默认适配器 - } -}; - -const adapter = createAGUIAdapter(aguiConfig); -const config = adapter.wrapConfig({ - endpoint: '/api/chat/tdesign-format', - stream: true -}); -``` - -**服务端响应格式**(现有TDesign格式): -```json -{"type": "text", "msg": "你好!"} -{"type": "thinking", "content": "正在思考...", "title": "思考中"} -``` - -**自动转换为**: -```json -{ - "type": "TEXT_MESSAGE_CHUNK", - "data": {"content": "你好!", "contentType": "text"} -} -{ - "type": "CUSTOM", - "data": {"type": "thinking", "content": "正在思考...", "title": "思考中"} -} -``` - -### 场景 3:传统回调模式 - -**适用于**:现有项目无需 AG-UI 标准化 - -```typescript -const config = { - endpoint: '/api/chat/traditional', - stream: true, - callbacks: { - onMessage: (chunk) => { - // 自定义业务逻辑 - if (chunk.data?.type === 'text') { - return { - type: 'text', - data: chunk.data.msg, - strategy: 'append' - }; - } - return null; - }, - onComplete: (isAborted, params, result) => { - console.log('对话完成'); - } - } -}; - -// 直接使用,无需适配器 -const engine = new ChatEngine(); -engine.init(config); -``` - -## API 参考 - -### AGUIConfig - -```typescript -interface AGUIConfig { - /** 使用模式 */ - mode: 'disabled' | 'native' | 'adapter'; - - /** Agent ID */ - agentId?: string; - - /** 是否启用双向通信 */ - bidirectional?: boolean; - - /** AG-UI事件处理器 */ - onEvent?: (event: AGUIEvent) => void; - - /** 自定义内容解析器 */ - contentParser?: (event: AGUIEvent) => AIContentChunkUpdate | null; - - /** 自定义适配器(adapter模式) */ - customAdapter?: (chunk: SSEChunkData) => AGUIEvent | null; -} -``` - -### AG-UI 标准事件类型 - -- **生命周期事件**:`RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` -- **文本消息事件**:`TEXT_MESSAGE_START`, `TEXT_MESSAGE_CHUNK`, `TEXT_MESSAGE_END` -- **工具调用事件**:`TOOL_CALL_START`, `TOOL_CALL_CHUNK`, `TOOL_CALL_END` -- **状态管理事件**:`STATE_SNAPSHOT`, `STATE_DELTA`, `MESSAGES_SNAPSHOT` -- **扩展事件**:`RAW`, `CUSTOM` - -### 工具函数 - -```typescript -// 创建适配器 -const adapter = createAGUIAdapter(config); - -// 事件类型检查 -import { AGUIUtils } from './adapters/agui-adapter'; - -AGUIUtils.isTextEvent(event); // 检查是否为文本事件 -AGUIUtils.isToolEvent(event); // 检查是否为工具事件 -AGUIUtils.isLifecycleEvent(event); // 检查是否为生命周期事件 -AGUIUtils.isStateEvent(event); // 检查是否为状态事件 -``` - -## 默认适配规则 - -### TDesign → AG-UI 转换 - -| TDesign 格式 | AG-UI 事件类型 | 说明 | -|-------------|---------------|------| -| `{type: "text", msg: "..."}` | `TEXT_MESSAGE_CHUNK` | 文本消息 | -| `{type: "markdown", msg: "..."}` | `TEXT_MESSAGE_CHUNK` | Markdown消息 | -| `{type: "thinking", content: "..."}` | `CUSTOM` | 思考过程 | -| `{type: "search", query: "..."}` | `CUSTOM` | 搜索动作 | -| 字符串 | `TEXT_MESSAGE_CHUNK` | 纯文本 | - -### AG-UI → AIContentChunkUpdate 转换 - -| AG-UI 事件 | TDesign 内容类型 | 策略 | -|-----------|----------------|------| -| `TEXT_MESSAGE_CHUNK` | `text` / `markdown` | `append` | -| `TOOL_CALL_*` | `search` | `append` | -| `CUSTOM` (thinking) | `thinking` | `append` | -| `RUN_ERROR` | `text` | `append` | - -## 最佳实践 - -### 1. 选择合适的模式 - -- **新项目 + AG-UI服务端** → `native` 模式 -- **现有项目 + 需要标准化** → `adapter` 模式 -- **现有项目 + 无需AG-UI** → 传统回调模式 - -### 2. 事件处理 - -```typescript -// ✅ 好的做法:分离关注点 -onEvent: (event) => { - // AG-UI协议层:发送到外部系统 - websocket.send(JSON.stringify(event)); - analytics.track('agui_event', event); - - // 不要在这里处理UI业务逻辑 -} - -// ✅ 业务逻辑在内容解析器中处理 -contentParser: (event) => { - if (event.type === 'TEXT_MESSAGE_CHUNK') { - updateChatUI(event.data.content); - return { - type: 'text', - data: event.data.content, - strategy: 'append' - }; - } - return null; -} -``` - -### 3. 自定义适配器 - -```typescript -customAdapter: (chunk) => { - // 只处理特殊格式,其他交给默认适配器 - if (chunk.data?.type === 'special_format') { - return { - type: 'CUSTOM', - data: transformSpecialFormat(chunk.data) - }; - } - return null; // 使用默认适配器 -} -``` - -## 示例代码 - -完整的使用示例请参考:`src/chatbot/_example/agui-scenarios-example.tsx` - -## 相关资源 - -- [AG-UI 官方文档](https://docs.ag-ui.com/) -- [AG-UI GitHub](https://github.com/ag-ui-protocol/ag-ui) -- [TDesign Chatbot 文档](./README.md) \ No newline at end of file diff --git a/packages/pro-components/chat/chatbot/core/adapters/README.md b/packages/pro-components/chat/chatbot/core/adapters/README.md deleted file mode 100644 index 3624eeceda..0000000000 --- a/packages/pro-components/chat/chatbot/core/adapters/README.md +++ /dev/null @@ -1,454 +0,0 @@ -# TDesign Chatbot AG-UI 协议适配器 - -为TDesign Web Components的Chatbot组件提供AG-UI协议支持,支持与标准化AI代理通信协议的无缝集成。 - -## 🎯 设计目标 - -- **配置分离**:网络配置、业务回调、协议转换完全独立 -- **互斥模式**:传统回调与AG-UI事件处理二选一,避免混淆 -- **向后兼容**:不启用时零影响,启用时可选择兼容模式 -- **职责清晰**:业务逻辑、协议通信、技术监控分层处理 - -## 📋 三种配置模式 - -### 1. 传统回调模式 - -使用原有的`callbacks`配置,适合现有项目迁移: - -```typescript -const config: ChatServiceConfig = { - // 网络配置 - endpoint: 'http://localhost:3000/sse/chat', - stream: true, - retryInterval: 1000, - maxRetries: 3, - - // 传统业务回调 - callbacks: { - onRequest: (params) => { - console.log('发送请求:', params); - return { headers: { 'Content-Type': 'application/json' } }; - }, - - onMessage: (chunk, message) => { - console.log('收到消息:', chunk); - // 解析并返回内容 - return { type: 'text', data: chunk.data }; - }, - - onComplete: (isAborted) => { - console.log('对话完成:', isAborted); - }, - - onError: (error) => { - console.error('处理错误:', error); - }, - }, - - // 连接技术监控 - connection: { - onHeartbeat: (event) => console.log('连接心跳'), - onConnectionStateChange: (event) => console.log('连接状态变化'), - }, -}; -``` - -**特点:** -- ✅ 使用熟悉的回调API -- ✅ 适合现有项目无缝迁移 -- ❌ 无AG-UI协议功能 - -### 2. AG-UI纯模式(推荐新项目) - -完全基于AG-UI事件驱动,不使用传统回调: - -```typescript -const config: ChatServiceConfig = { - // 网络配置 - endpoint: 'http://localhost:3000/sse/chat', - stream: true, - - // ⚠️ 注意:AG-UI纯模式下不配置callbacks! - // callbacks: undefined, - - // AG-UI协议配置 - agui: { - enabled: true, - agentId: 'my-chatbot', - bidirectional: true, - - // 业务逻辑处理(替代传统callbacks) - onBusinessEvent: (event: AGUIEvent) => { - console.log('AG-UI业务事件:', event); - - switch (event.type) { - case 'RUN_STARTED': - console.log('🚀 对话开始'); - break; - - case 'TEXT_MESSAGE_CHUNK': - console.log('📝 接收文本:', event.data.content); - // 在这里处理UI更新逻辑 - updateChatUI(event.data.content); - break; - - case 'TOOL_CALL_CHUNK': - console.log('🔧 工具调用:', event.data.toolName); - break; - - case 'RUN_FINISHED': - console.log('✅ 对话完成:', event.data.reason); - enableInputField(); - break; - - case 'RUN_ERROR': - console.error('❌ 运行错误:', event.data.error); - showErrorMessage(event.data.error); - break; - } - }, - - // 协议通信(发送到外部系统) - onProtocolEvent: (event: AGUIEvent) => { - console.log('📡 协议事件:', event.type); - - // 发送到外部AG-UI兼容系统 - websocket.send(JSON.stringify(event)); - analytics.track('agui_event', event); - messageQueue.publish('agui-events', event); - }, - - // 外部事件处理(双向通信) - onExternalEvent: (event: AGUIEvent) => { - console.log('🔄 外部事件:', event); - // 处理外部系统发送的AG-UI事件 - }, - }, -}; -``` - -**特点:** -- ✅ 完全基于AG-UI标准事件 -- ✅ 支持双向通信 -- ✅ 现代化事件驱动架构 -- ✅ 与外部AG-UI系统无缝集成 -- ❌ 需要学习AG-UI事件API - -### 3. 传统兼容模式 - -同时支持传统回调和AG-UI协议,适合渐进迁移: - -```typescript -const config: ChatServiceConfig = { - // 网络配置 - endpoint: 'http://localhost:3000/sse/chat', - stream: true, - - // 传统业务回调(保持原有逻辑不变) - callbacks: { - onMessage: (chunk, message) => { - console.log('💬 传统业务处理:', chunk); - // 原有的业务逻辑保持不变 - return { type: 'text', data: String(chunk.data) }; - }, - - onComplete: (isAborted) => { - console.log('🏁 传统完成处理:', isAborted); - enableInputField(); - }, - - onError: (error) => { - console.error('🚨 传统错误处理:', error); - showErrorMessage(error); - }, - }, - - // 同时启用AG-UI协议转换 - agui: { - enabled: true, - agentId: 'compatibility-bot', - - // 仅用于协议通信,不处理业务逻辑 - onProtocolEvent: (event: AGUIEvent) => { - console.log('📡 AG-UI协议事件:', event.type); - - // 发送到外部AG-UI兼容系统 - websocket.send(JSON.stringify(event)); - fetch('/api/agui-events', { - method: 'POST', - body: JSON.stringify(event) - }); - }, - }, -}; -``` - -**特点:** -- ✅ 保持原有业务逻辑不变 -- ✅ 增加AG-UI协议支持 -- ✅ 业务逻辑与协议通信分离 -- ✅ 适合现有项目渐进迁移 -- ⚠️ 两套API同时存在 - -## 🔧 AG-UI事件类型 - -AG-UI协议定义了16种标准事件类型: - -| 事件类型 | 描述 | 数据结构 | -|---------|------|----------| -| `RUN_STARTED` | 对话开始 | `{ prompt, messageId, attachments }` | -| `TEXT_MESSAGE_CHUNK` | 文本消息块 | `{ content, messageId, contentType? }` | -| `TOOL_CALL_CHUNK` | 工具调用块 | `{ toolName, action, input }` | -| `TOOL_RESULT_CHUNK` | 工具结果块 | `{ toolName, result, success }` | -| `INPUT_REQUEST` | 请求用户输入 | `{ requestId, prompt, options }` | -| `RUN_FINISHED` | 对话结束 | `{ success, reason, result? }` | -| `RUN_ERROR` | 运行错误 | `{ error, details }` | -| `HEARTBEAT` | 心跳检测 | `{ connectionId, timestamp }` | -| `STATE_CHANGE` | 状态变化 | `{ from, to, connectionId }` | -| `CONNECTION_ESTABLISHED` | 连接建立 | `{ connectionId }` | -| `CONNECTION_LOST` | 连接断开 | `{ connectionId, reason }` | -| `USER_INPUT` | 用户输入 | `{ requestId, input }` | -| `AGENT_MESSAGE` | 代理消息 | `{ type, content, title? }` | -| `SYSTEM_MESSAGE` | 系统消息 | `{ content, level }` | -| `METADATA_UPDATE` | 元数据更新 | `{ type, data }` | - -## 🎨 使用示例 - -### 基础使用 - -```tsx -import { Component } from 'omi'; -import type { ChatServiceConfig } from 'tdesign-web-components/chatbot'; - -export default class MyChatBot extends Component { - // AG-UI纯模式配置 - chatConfig: ChatServiceConfig = { - endpoint: '/api/chat', - stream: true, - - agui: { - enabled: true, - agentId: 'my-assistant', - bidirectional: true, - - onBusinessEvent: (event) => { - // 处理所有业务逻辑 - this.handleBusinessEvent(event); - }, - - onProtocolEvent: (event) => { - // 发送到外部系统 - this.sendToExternalSystem(event); - }, - }, - }; - - handleBusinessEvent(event) { - switch (event.type) { - case 'TEXT_MESSAGE_CHUNK': - // 更新UI显示 - this.updateChatDisplay(event.data.content); - break; - case 'RUN_FINISHED': - // 启用输入框 - this.enableInput(); - break; - } - } - - render() { - return ( - console.log('聊天就绪')} - /> - ); - } -} -``` - -### 双向通信 - -```typescript -// 请求用户输入 -const userInput = await chatEngine.requestUserInput( - '请选择你的偏好设置:', - { type: 'select', options: ['A', 'B', 'C'] } -); - -// 处理外部AG-UI事件 -chatEngine.handleAGUIEvent({ - type: 'USER_INPUT', - data: { requestId: 'req_123', input: 'A' }, - timestamp: Date.now(), -}); -``` - -### 自定义事件映射 - -```typescript -const config: ChatServiceConfig = { - agui: { - enabled: true, - - // 自定义事件映射 - eventMapping: { - 'TEXT_MESSAGE_CHUNK': 'custom_text', - 'RUN_STARTED': 'session_begin', - 'RUN_FINISHED': 'session_end', - }, - - onProtocolEvent: (event) => { - // 事件类型已经被映射 - console.log('映射后的事件:', event.type); - }, - }, -}; -``` - -## 📚 配置对比表 - -| 配置项 | 传统模式 | AG-UI纯模式 | 兼容模式 | -|-------|---------|------------|----------| -| `callbacks` | ✅ 必需 | ❌ 不使用 | ✅ 保留 | -| `agui.enabled` | ❌ 不启用 | ✅ 必需 | ✅ 启用 | -| `agui.onBusinessEvent` | ❌ 不使用 | ✅ 必需 | ❌ 不使用 | -| `agui.onProtocolEvent` | ❌ 不使用 | ✅ 可选 | ✅ 推荐 | -| 业务逻辑处理 | callbacks | onBusinessEvent | callbacks | -| AG-UI协议支持 | 无 | 完整 | 仅协议转换 | -| 迁移难度 | 无需迁移 | 需要重写 | 无需更改 | -| 推荐场景 | 现有项目 | 新项目 | 渐进迁移 | - -## 🔍 调试和监控 - -### 获取适配器状态 - -```typescript -const adapter = chatEngine.getAGUIAdapter(); -if (adapter) { - console.log('适配器状态:', adapter.getState()); -} -``` - -### 监听协议事件 - -```typescript -// 监听所有AG-UI协议事件 -window.addEventListener('agui-protocol-event', (event) => { - console.log('收到AG-UI事件:', event.detail); -}); -``` - -### 调试日志 - -启用AG-UI适配器后,控制台会显示详细的运行模式信息: - -``` -🤖 [TDesign-Chatbot] AG-UI协议适配器已启用 - AG-UI纯模式 -{ - agentId: "my-chatbot", - bidirectional: true, - mode: "AG-UI纯模式", - hasCallbacks: false, - hasBusinessEvent: true -} -``` - -## 🚀 最佳实践 - -### 1. 选择合适的模式 - -- **新项目**:使用AG-UI纯模式,获得最佳的事件驱动体验 -- **现有项目**:使用传统兼容模式,渐进式增加AG-UI支持 -- **简单项目**:使用传统模式,保持简单 - -### 2. 事件处理分离 - -```typescript -// ✅ 正确:职责分离 -const config = { - agui: { - enabled: true, - - // 业务逻辑:处理UI更新、状态管理 - onBusinessEvent: (event) => { - updateUI(event); - updateState(event); - }, - - // 协议通信:发送到外部系统 - onProtocolEvent: (event) => { - websocket.send(JSON.stringify(event)); - analytics.track('agui_event', event); - }, - }, -}; - -// ❌ 错误:职责混淆 -const config = { - agui: { - onProtocolEvent: (event) => { - // 不要在协议层处理业务逻辑 - updateUI(event); // 错误! - websocket.send(JSON.stringify(event)); // 正确 - }, - }, -}; -``` - -### 3. 错误处理 - -```typescript -const config = { - agui: { - enabled: true, - - onBusinessEvent: (event) => { - try { - handleBusinessLogic(event); - } catch (error) { - console.error('业务逻辑错误:', error); - // 不要让业务错误影响协议通信 - } - }, - - onProtocolEvent: (event) => { - try { - sendToExternalSystem(event); - } catch (error) { - console.error('协议通信错误:', error); - // 协议错误不应影响主业务流程 - } - }, - }, -}; -``` - -## 🔗 相关链接 - -- [AG-UI协议官方文档](https://docs.ag-ui.com) -- [TDesign Chatbot组件文档](../README.md) -- [示例代码](../_example/agui-clear-example.tsx) - -## 📋 FAQ - -### Q: 如何从传统模式迁移到AG-UI模式? - -A: 推荐使用传统兼容模式作为过渡: - -1. 启用AG-UI适配器但保留原有callbacks -2. 逐步将业务逻辑迁移到onBusinessEvent -3. 最后删除callbacks配置 - -### Q: 可以同时使用callbacks和onBusinessEvent吗? - -A: 不建议。两者是互斥的: -- 有callbacks:传统兼容模式,onBusinessEvent不生效 -- 无callbacks:AG-UI纯模式,使用onBusinessEvent - -### Q: AG-UI协议事件与传统回调有什么区别? - -A: 主要区别: -- **传统回调**:函数式API,直接处理SSE数据 -- **AG-UI事件**:标准化事件格式,包含runId、agentId等元数据 -- **适用场景**:AG-UI适合多代理通信,传统回调适合简单场景 \ No newline at end of file diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts b/packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts deleted file mode 100644 index a62c21856f..0000000000 --- a/packages/pro-components/chat/chatbot/core/adapters/agui-adapter.ts +++ /dev/null @@ -1,561 +0,0 @@ -/** - * AG-UI Protocol Adapter - * - * AG-UI协议适配器 - 支持三种使用场景: - * 1. 服务端完全按照AG-UI协议返回(直接解析AG-UI事件) - * 2. 服务端未按照AG-UI协议,业务提供适配器转换 - * 3. 服务端未按照AG-UI协议,保持传统回调模式 - */ - -import type { - AIContentChunkUpdate, - ChatMessagesData, - ChatRequestParams, - ChatServiceConfig, - SSEChunkData, -} from '../type'; - -// AG-UI适配器配置(前向声明) -export interface AGUIAdapterConfig { - /** 是否启用AG-UI协议适配 */ - enabled: boolean; - /** Agent ID,用于标识当前AI代理 */ - agentId?: string; - /** 是否启用双向通信(支持INPUT_REQUEST) */ - bidirectional?: boolean; - /** 自定义事件映射 */ - eventMapping?: Partial>; - /** AG-UI事件处理器 */ - onProtocolEvent?: (event: any) => void; - /** 发送AG-UI事件的处理器 */ - onExternalEvent?: (event: any) => void; - /** AG-UI业务事件处理器 - AG-UI纯模式下替代传统callbacks */ - onBusinessEvent?: (event: any) => void; -} - -// ============================================================================= -// AG-UI 标准协议事件类型(严格遵循官方协议) -// ============================================================================= - -/** AG-UI协议标准事件类型 */ -export type AGUIEventType = - // 生命周期事件 - | 'RUN_STARTED' // 对话开始 - | 'RUN_FINISHED' // 对话完成 - | 'RUN_ERROR' // 对话出错 - | 'STEP_STARTED' // 步骤开始 - | 'STEP_FINISHED' // 步骤完成 - - // 文本消息事件 - | 'TEXT_MESSAGE_START' // 文本消息开始 - | 'TEXT_MESSAGE_CHUNK' // 文本消息块(流式) - | 'TEXT_MESSAGE_END' // 文本消息结束 - - // 工具调用事件 - | 'TOOL_CALL_START' // 工具调用开始 - | 'TOOL_CALL_CHUNK' // 工具调用块 - | 'TOOL_CALL_END' // 工具调用结束 - - // 状态管理事件 - | 'STATE_SNAPSHOT' // 状态快照 - | 'STATE_DELTA' // 状态增量更新 - | 'MESSAGES_SNAPSHOT' // 消息快照 - - // 扩展事件 - | 'RAW' // 原始事件 - | 'CUSTOM'; // 自定义事件 - -// AG-UI 标准事件数据结构 -export interface AGUIEvent { - type: AGUIEventType; - data: T; - timestamp?: number; - runId?: string; - agentId?: string; - messageId?: string; - threadId?: string; - metadata?: Record; -} - -// ============================================================================= -// AG-UI 使用配置 -// ============================================================================= - -/** AG-UI使用模式 */ -export type AGUIMode = - | 'disabled' // 禁用AG-UI,使用传统回调 - | 'native' // 服务端原生AG-UI协议 - | 'adapter'; // 服务端非AG-UI,需要业务提供适配器 - -/** AG-UI配置 */ -export interface AGUIConfig { - /** 使用模式 */ - mode: AGUIMode; - - /** Agent ID */ - agentId?: string; - - /** 是否启用双向通信 */ - bidirectional?: boolean; - - /** - * AG-UI事件处理器 - * - native模式:处理服务端直接返回的AG-UI事件 - * - adapter模式:处理适配器转换后的AG-UI事件 - */ - onEvent?: (event: AGUIEvent) => void; - - /** - * 自定义内容解析器 - * 如果不提供,使用默认解析器将AG-UI事件转换为AIContentChunkUpdate - */ - contentParser?: (event: AGUIEvent) => AIContentChunkUpdate | null; - - /** - * 自定义适配器(adapter模式下必需) - * 将业务自定义的chunk格式转换为AG-UI标准格式 - * - * 注意:TDesign Chatbot本身没有标准的chunk格式, - * 每个业务的chunk.data结构都不同,因此必须提供此函数 - */ - customAdapter?: (chunk: SSEChunkData) => AGUIEvent | null; -} - -// ============================================================================= -// 默认内容解析器:AG-UI事件 → AIContentChunkUpdate -// ============================================================================= - -/** - * 默认AG-UI事件解析器 - * 将AG-UI标准事件转换为组件渲染需要的AIContentChunkUpdate结构 - */ -export function parseAGUIEventToContent(event: AGUIEvent): AIContentChunkUpdate | null { - switch (event.type) { - case 'TEXT_MESSAGE_CHUNK': - return { - type: event.data.contentType === 'markdown' ? 'markdown' : 'text', - data: event.data.content || event.data.text || '', - strategy: 'append' as const, - }; - - case 'TEXT_MESSAGE_START': - return { - type: 'text', - data: '', - strategy: 'append' as const, - }; - - case 'TEXT_MESSAGE_END': - return { - type: 'text', - data: event.data.finalContent || '', - strategy: 'append' as const, - }; - - case 'TOOL_CALL_START': - case 'TOOL_CALL_CHUNK': - case 'TOOL_CALL_END': - return { - type: 'search', - data: { - title: event.data.toolName || 'Tool Call', - references: [], - }, - strategy: 'append' as const, - }; - - case 'RUN_ERROR': - return { - type: 'text', - data: event.data.error || event.data.message || 'Unknown error', - strategy: 'append' as const, - }; - - case 'CUSTOM': - // 处理自定义事件,尝试解析为通用格式 - if (event.data.type === 'thinking') { - return { - type: 'thinking', - data: { - text: event.data.content || event.data.text || '', - title: event.data.title, - }, - strategy: 'append' as const, - }; - } - - if (event.data.type === 'search') { - return { - type: 'search', - data: { - title: 'Search', - references: [], - }, - strategy: 'append' as const, - }; - } - - return { - type: 'text', - data: event.data.content || event.data.text || JSON.stringify(event.data), - strategy: 'append' as const, - }; - - default: - // 忽略生命周期事件(RUN_STARTED, RUN_FINISHED等) - return null; - } -} - -// ============================================================================= -// AG-UI 适配器类 -// ============================================================================= - -export class AGUIAdapter { - private config: AGUIConfig; - - private currentRunId: string | null = null; - - constructor(config: AGUIConfig) { - this.config = { - agentId: 'tdesign-chatbot', - bidirectional: false, - ...config, - }; - } - - /** - * 包装ChatServiceConfig以支持AG-UI - */ - public wrapConfig(originalConfig: ChatServiceConfig): ChatServiceConfig { - if (this.config.mode === 'disabled') { - return originalConfig; - } - - // adapter模式下检查必需的customAdapter - if (this.config.mode === 'adapter' && !this.config.customAdapter) { - throw new Error( - '[AGUIAdapter] adapter模式下必须提供customAdapter函数,' + - '因为TDesign Chatbot本身没有标准的chunk格式,' + - '每个业务的chunk.data结构都不同', - ); - } - - return { - ...originalConfig, - callbacks: this.createAGUICallbacks(originalConfig), - }; - } - - /** - * 创建AG-UI模式的回调配置 - */ - private createAGUICallbacks(originalConfig: ChatServiceConfig) { - return { - onRequest: (params: ChatRequestParams) => { - this.currentRunId = this.generateRunId(); - - // 发送RUN_STARTED事件 - this.emitEvent({ - type: 'RUN_STARTED', - data: { - prompt: params.prompt, - messageId: params.messageID, - attachments: params.attachments, - }, - runId: this.currentRunId, - agentId: this.config.agentId, - }); - - return originalConfig.callbacks?.onRequest?.(params) || {}; - }, - - onMessage: (chunk: SSEChunkData, message?: ChatMessagesData) => { - const aguiEvent = this.processChunk(chunk); - - if (aguiEvent) { - // 发送AG-UI事件 - this.emitEvent(aguiEvent); - - // 解析为内容并返回给组件 - const content = this.parseEventToContent(aguiEvent); - if (content) { - return content; - } - } - - // 如果AG-UI解析失败,回退到原有逻辑 - return originalConfig.callbacks?.onMessage?.(chunk, message); - }, - - onComplete: (isAborted: boolean, params: RequestInit, result?: any) => { - // 发送RUN_FINISHED事件 - this.emitEvent({ - type: 'RUN_FINISHED', - data: { - success: !isAborted, - reason: isAborted ? 'user_aborted' : 'completed', - result, - }, - runId: this.currentRunId, - agentId: this.config.agentId, - }); - - this.currentRunId = null; - return originalConfig.callbacks?.onComplete?.(isAborted, params, result); - }, - - onError: (err: Error | Response) => { - // 发送RUN_ERROR事件 - this.emitEvent({ - type: 'RUN_ERROR', - data: { - error: err instanceof Error ? err.message : 'Request failed', - details: err, - }, - runId: this.currentRunId, - agentId: this.config.agentId, - }); - - this.currentRunId = null; - return originalConfig.callbacks?.onError?.(err); - }, - - onAbort: async () => { - this.emitEvent({ - type: 'RUN_FINISHED', - data: { - success: false, - reason: 'user_aborted', - }, - runId: this.currentRunId, - agentId: this.config.agentId, - }); - - this.currentRunId = null; - return originalConfig.callbacks?.onAbort?.(); - }, - }; - } - - /** - * 处理数据块,根据模式选择不同的处理方式 - */ - private processChunk(chunk: SSEChunkData): AGUIEvent | null { - switch (this.config.mode) { - case 'native': - // 场景1:服务端直接返回AG-UI格式 - return this.parseNativeAGUIChunk(chunk); - - case 'adapter': - // 场景2:需要业务提供适配转换 - return this.adaptChunkToAGUI(chunk); - - default: - return null; - } - } - - /** - * 解析原生AG-UI格式数据块 - * 服务端直接返回AG-UI标准格式:{type: 'TEXT_MESSAGE_CHUNK', data: {...}} - */ - private parseNativeAGUIChunk(chunk: SSEChunkData): AGUIEvent | null { - try { - const chunkData = chunk.data; - - // 检查是否为AG-UI标准格式 - if (chunkData && typeof chunkData === 'object' && 'type' in chunkData) { - return { - type: chunkData.type as AGUIEventType, - data: chunkData.data || chunkData, - timestamp: chunkData.timestamp || Date.now(), - runId: chunkData.runId || this.currentRunId, - agentId: chunkData.agentId || this.config.agentId, - messageId: chunkData.messageId, - threadId: chunkData.threadId, - metadata: chunkData.metadata, - }; - } - - // 如果不是标准格式,当作TEXT_MESSAGE_CHUNK处理 - if (typeof chunkData === 'string') { - return { - type: 'TEXT_MESSAGE_CHUNK', - data: { content: chunkData }, - timestamp: Date.now(), - runId: this.currentRunId, - agentId: this.config.agentId, - }; - } - } catch (error) { - console.warn('[AGUIAdapter] 解析原生AG-UI数据失败:', error); - } - - return null; - } - - /** - * 适配业务自定义格式到AG-UI格式 - * 业务必须提供customAdapter,因为TDesign没有标准chunk格式 - */ - private adaptChunkToAGUI(chunk: SSEChunkData): AGUIEvent | null { - try { - // 必须使用业务提供的自定义适配器 - if (this.config.customAdapter) { - return this.config.customAdapter(chunk); - } - - // 如果没有提供适配器,返回null(应该在wrapConfig时就检查了) - console.warn('[AGUIAdapter] adapter模式下必须提供customAdapter'); - return null; - } catch (error) { - console.warn('[AGUIAdapter] 适配数据格式失败:', error); - return null; - } - } - - /** - * 将AG-UI事件解析为内容 - */ - private parseEventToContent(event: AGUIEvent): AIContentChunkUpdate | null { - try { - // 使用自定义解析器 - if (this.config.contentParser) { - return this.config.contentParser(event); - } - - // 使用默认解析器 - return parseAGUIEventToContent(event); - } catch (error) { - console.warn('[AGUIAdapter] 解析事件内容失败:', error); - return null; - } - } - - /** - * 发送AG-UI事件 - */ - private emitEvent(event: AGUIEvent): void { - try { - // 确保事件格式完整 - const completeEvent: AGUIEvent = { - timestamp: Date.now(), - runId: this.currentRunId, - agentId: this.config.agentId, - ...event, - }; - - // 调用事件处理器 - this.config.onEvent?.(completeEvent); - } catch (error) { - console.error('[AGUIAdapter] 发送AG-UI事件失败:', error); - } - } - - /** - * 生成运行ID - */ - private generateRunId(): string { - return `run_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * 更新配置 - */ - public updateConfig(newConfig: Partial): void { - this.config = { ...this.config, ...newConfig }; - } - - /** - * 获取当前运行ID - */ - public getCurrentRunId(): string | null { - return this.currentRunId; - } -} - -// ============================================================================= -// 工厂函数和工具 -// ============================================================================= - -/** - * 创建AG-UI适配器 - */ -export function createAGUIAdapter(config: AGUIConfig): AGUIAdapter { - return new AGUIAdapter(config); -} - -/** - * AG-UI事件类型检查工具 - */ -export const AGUIUtils = { - isTextEvent: (event: AGUIEvent): boolean => - ['TEXT_MESSAGE_START', 'TEXT_MESSAGE_CHUNK', 'TEXT_MESSAGE_END'].includes(event.type), - - isToolEvent: (event: AGUIEvent): boolean => - ['TOOL_CALL_START', 'TOOL_CALL_CHUNK', 'TOOL_CALL_END'].includes(event.type), - - isLifecycleEvent: (event: AGUIEvent): boolean => - ['RUN_STARTED', 'RUN_FINISHED', 'RUN_ERROR', 'STEP_STARTED', 'STEP_FINISHED'].includes(event.type), - - isStateEvent: (event: AGUIEvent): boolean => - ['STATE_SNAPSHOT', 'STATE_DELTA', 'MESSAGES_SNAPSHOT'].includes(event.type), -}; - -// ============================================================================= -// 业务适配器示例(仅供参考) -// ============================================================================= - -/** - * 示例:某个具体业务的适配器 - * 这只是个示例,实际使用时业务需要根据自己的chunk格式编写 - */ -export function createExampleBusinessAdapter() { - return (chunk: SSEChunkData): AGUIEvent | null => { - const chunkData = chunk.data; - - // 示例业务格式1:纯文本 - if (typeof chunkData === 'string') { - return { - type: 'TEXT_MESSAGE_CHUNK', - data: { - content: chunkData, - contentType: 'text', - }, - timestamp: Date.now(), - }; - } - - // 示例业务格式2:结构化数据 - if (chunkData && typeof chunkData === 'object') { - // 假设业务定义了这样的格式 - if (chunkData.msgType === 'text') { - return { - type: 'TEXT_MESSAGE_CHUNK', - data: { - content: chunkData.content, - contentType: 'text', - }, - timestamp: Date.now(), - }; - } - - if (chunkData.msgType === 'thinking') { - return { - type: 'CUSTOM', - data: { - type: 'thinking', - content: chunkData.thought, - title: chunkData.thinkingTitle, - }, - timestamp: Date.now(), - }; - } - - // 处理其他业务自定义格式... - } - - return null; - }; -} diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts index 89cb8bda5a..f75accd291 100644 --- a/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-event-mapper.ts @@ -1,38 +1,35 @@ /* eslint-disable class-methods-use-this */ -import type { AIMessageContent, SSEChunkData } from '../../type'; +import type { AIContentChunkUpdate, SSEChunkData } from '../../type'; import { EventType } from './events'; /** * AGUIEventMapper - * 将AG-UI协议事件(SSEChunkData)转换为AIMessageContent[] + * 将AG-UI协议事件(SSEChunkData)转换为AIContentChunkUpdate * 支持多轮对话、增量文本、工具调用、思考、状态快照、消息快照等基础事件 */ export class AGUIEventMapper { - private currentMessageId: string | null = null; - - private currentContent: AIMessageContent[] = []; - private toolCallMap: Record = {}; /** - * 主入口:将SSE事件转换为AIMessageContent[] + * 主入口:将SSE事件转换为AIContentChunkUpdate */ - mapEvent(chunk: SSEChunkData): AIMessageContent | AIMessageContent[] | null { + mapEvent(chunk: SSEChunkData): AIContentChunkUpdate | AIContentChunkUpdate[] | null { const event = chunk.data; if (!event?.type) return null; switch (event.type) { - case 'TEXT_MESSAGE_START': + case EventType.TEXT_MESSAGE_START: return { type: 'markdown', status: 'streaming', data: '', strategy: 'append', }; - case 'TEXT_MESSAGE_CHUNK': - case 'TEXT_MESSAGE_END': + case EventType.TEXT_MESSAGE_CHUNK: + case EventType.TEXT_MESSAGE_CONTENT: + case EventType.TEXT_MESSAGE_END: return { type: 'markdown', - status: event.type === 'TEXT_MESSAGE_END' ? 'complete' : 'streaming', + status: event.type === EventType.TEXT_MESSAGE_END ? 'complete' : 'streaming', data: event.delta || '', strategy: 'merge', }; @@ -101,7 +98,7 @@ export class AGUIEventMapper { } } - private handleStateSnapshot(snapshot: any): AIMessageContent[] { + private handleStateSnapshot(snapshot: any): AIContentChunkUpdate[] { // 只取assistant消息 if (!snapshot?.messages) return []; return snapshot.messages.flatMap((msg: any) => { @@ -116,7 +113,7 @@ export class AGUIEventMapper { }); } - private handleMessagesSnapshot(messages: any[]): AIMessageContent[] { + private handleMessagesSnapshot(messages: any[]): AIContentChunkUpdate[] { // 只取assistant消息 if (!messages) return []; return messages.flatMap((msg: any) => { @@ -131,11 +128,11 @@ export class AGUIEventMapper { }); } - private handleCustomEvent(event: any): AIMessageContent { + private handleCustomEvent(event: any): AIContentChunkUpdate { if (event.name === 'suggestion') { return { type: 'suggestion', - data: event.value?.suggestions || [], + data: event.value || [], status: 'complete', }; } @@ -148,8 +145,8 @@ export class AGUIEventMapper { } reset() { - this.currentMessageId = null; - this.currentContent = []; + // this.currentMessageId = null; + // this.currentContent = []; this.toolCallMap = {}; } } diff --git a/packages/pro-components/chat/chatbot/core/adapters/agui/agui-protocol.txt b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-protocol.txt new file mode 100644 index 0000000000..fdf9bb1782 --- /dev/null +++ b/packages/pro-components/chat/chatbot/core/adapters/agui/agui-protocol.txt @@ -0,0 +1,6406 @@ +# Agents +Source: https://docs.ag-ui.com/concepts/agents + +Learn about agents in the Agent User Interaction Protocol + +# Agents + +Agents are the core components in the AG-UI protocol that process requests and +generate responses. They establish a standardized way for front-end applications +to communicate with AI services through a consistent interface, regardless of +the underlying implementation. + +## What is an Agent? + +In AG-UI, an agent is a class that: + +1. Manages conversation state and message history +2. Processes incoming messages and context +3. Generates responses through an event-driven streaming interface +4. Follows a standardized protocol for communication + +Agents can be implemented to connect with any AI service, including: + +* Large language models (LLMs) like GPT-4 or Claude +* Custom AI systems +* Retrieval augmented generation (RAG) systems +* Multi-agent systems + +## Agent Architecture + +All agents in AG-UI extend the `AbstractAgent` class, which provides the +foundation for: + +* State management +* Message history tracking +* Event stream processing +* Tool usage + +```typescript +import { AbstractAgent } from "@ag-ui/client" + +class MyAgent extends AbstractAgent { + protected run(input: RunAgentInput): RunAgent { + // Implementation details + } +} +``` + +### Core Components + +AG-UI agents have several key components: + +1. **Configuration**: Agent ID, thread ID, and initial state +2. **Messages**: Conversation history with user and assistant messages +3. **State**: Structured data that persists across interactions +4. **Events**: Standardized messages for communication with clients +5. **Tools**: Functions that agents can use to interact with external systems + +## Agent Types + +AG-UI provides different agent implementations to suit various needs: + +### AbstractAgent + +The base class that all agents extend. It handles core event processing, state +management, and message history. + +### HttpAgent + +A concrete implementation that connects to remote AI services via HTTP: + +```typescript +import { HttpAgent } from "@ag-ui/client" + +const agent = new HttpAgent({ + url: "https://your-agent-endpoint.com/agent", + headers: { + Authorization: "Bearer your-api-key", + }, +}) +``` + +### Custom Agents + +You can create custom agents to integrate with any AI service by extending +`AbstractAgent`: + +```typescript +class CustomAgent extends AbstractAgent { + // Custom properties and methods + + protected run(input: RunAgentInput): RunAgent { + // Implement the agent's logic + } +} +``` + +## Implementing Agents + +### Basic Implementation + +To create a custom agent, extend the `AbstractAgent` class and implement the +required `run` method: + +```typescript +import { + AbstractAgent, + RunAgent, + RunAgentInput, + EventType, + BaseEvent, +} from "@ag-ui/client" +import { Observable } from "rxjs" + +class SimpleAgent extends AbstractAgent { + protected run(input: RunAgentInput): RunAgent { + const { threadId, runId } = input + + return () => + new Observable((observer) => { + // Emit RUN_STARTED event + observer.next({ + type: EventType.RUN_STARTED, + threadId, + runId, + }) + + // Send a message + const messageId = Date.now().toString() + + // Message start + observer.next({ + type: EventType.TEXT_MESSAGE_START, + messageId, + role: "assistant", + }) + + // Message content + observer.next({ + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta: "Hello, world!", + }) + + // Message end + observer.next({ + type: EventType.TEXT_MESSAGE_END, + messageId, + }) + + // Emit RUN_FINISHED event + observer.next({ + type: EventType.RUN_FINISHED, + threadId, + runId, + }) + + // Complete the observable + observer.complete() + }) + } +} +``` + +## Agent Capabilities + +Agents in the AG-UI protocol provide a rich set of capabilities that enable +sophisticated AI interactions: + +### Interactive Communication + +Agents establish bi-directional communication channels with front-end +applications through event streams. This enables: + +* Real-time streaming responses character-by-character +* Immediate feedback loops between user and AI +* Progress indicators for long-running operations +* Structured data exchange in both directions + +### Tool Usage + +Agents can use tools to perform actions and access external resources. +Importantly, tools are defined and passed in from the front-end application to +the agent, allowing for a flexible and extensible system: + +```typescript +// Tool definition +const confirmAction = { + name: "confirmAction", + description: "Ask the user to confirm a specific action before proceeding", + parameters: { + type: "object", + properties: { + action: { + type: "string", + description: "The action that needs user confirmation", + }, + importance: { + type: "string", + enum: ["low", "medium", "high", "critical"], + description: "The importance level of the action", + }, + details: { + type: "string", + description: "Additional details about the action", + }, + }, + required: ["action"], + }, +} + +// Running an agent with tools from the frontend +agent.runAgent({ + tools: [confirmAction], // Frontend-defined tools passed to the agent + // other parameters +}) +``` + +Tools are invoked through a sequence of events: + +1. `TOOL_CALL_START`: Indicates the beginning of a tool call +2. `TOOL_CALL_ARGS`: Streams the arguments for the tool call +3. `TOOL_CALL_END`: Marks the completion of the tool call + +Front-end applications can then execute the tool and provide results back to the +agent. This bidirectional flow enables sophisticated human-in-the-loop workflows +where: + +* The agent can request specific actions be performed +* Humans can execute those actions with appropriate judgment +* Results are fed back to the agent for continued reasoning +* The agent maintains awareness of all decisions made in the process + +This mechanism is particularly powerful for implementing interfaces where AI and +humans collaborate. For example, [CopilotKit](https://docs.copilotkit.ai/) +leverages this exact pattern with their +[`useCopilotAction`](https://docs.copilotkit.ai/guides/frontend-actions) hook, +which provides a simplified way to define and handle tools in React +applications. + +By keeping the AI informed about human decisions through the tool mechanism, +applications can maintain context and create more natural collaborative +experiences between users and AI assistants. + +### State Management + +Agents maintain a structured state that persists across interactions. This state +can be: + +* Updated incrementally through `STATE_DELTA` events +* Completely refreshed with `STATE_SNAPSHOT` events +* Accessed by both the agent and front-end +* Used to store user preferences, conversation context, or application state + +```typescript +// Accessing agent state +console.log(agent.state.preferences) + +// State is automatically updated during agent runs +agent.runAgent().subscribe((event) => { + if (event.type === EventType.STATE_DELTA) { + // State has been updated + console.log("New state:", agent.state) + } +}) +``` + +### Multi-Agent Collaboration + +AG-UI supports agent-to-agent handoff and collaboration: + +* Agents can delegate tasks to other specialized agents +* Multiple agents can work together in a coordinated workflow +* State and context can be transferred between agents +* The front-end maintains a consistent experience across agent transitions + +For example, a general assistant agent might hand off to a specialized coding +agent when programming help is needed, passing along the conversation context +and specific requirements. + +### Human-in-the-Loop Workflows + +Agents support human intervention and assistance: + +* Agents can request human input on specific decisions +* Front-ends can pause agent execution and resume it after human feedback +* Human experts can review and modify agent outputs before they're finalized +* Hybrid workflows combine AI efficiency with human judgment + +This enables applications where the agent acts as a collaborative partner rather +than an autonomous system. + +### Conversational Memory + +Agents maintain a complete history of conversation messages: + +* Past interactions inform future responses +* Message history is synchronized between client and server +* Messages can include rich content (text, structured data, references) +* The context window can be managed to focus on relevant information + +```typescript +// Accessing message history +console.log(agent.messages) + +// Adding a new user message +agent.messages.push({ + id: "msg_123", + role: "user", + content: "Can you explain that in more detail?", +}) +``` + +### Metadata and Instrumentation + +Agents can emit metadata about their internal processes: + +* Reasoning steps through custom events +* Performance metrics and timing information +* Source citations and reference tracking +* Confidence scores for different response options + +This allows front-ends to provide transparency into the agent's decision-making +process and help users understand how conclusions were reached. + +## Using Agents + +Once you've implemented or instantiated an agent, you can use it like this: + +```typescript +// Create an agent instance +const agent = new HttpAgent({ + url: "https://your-agent-endpoint.com/agent", +}) + +// Add initial messages if needed +agent.messages = [ + { + id: "1", + role: "user", + content: "Hello, how can you help me today?", + }, +] + +// Run the agent +agent + .runAgent({ + runId: "run_123", + tools: [], // Optional tools + context: [], // Optional context + }) + .subscribe({ + next: (event) => { + // Handle different event types + switch (event.type) { + case EventType.TEXT_MESSAGE_CONTENT: + console.log("Content:", event.delta) + break + // Handle other events + } + }, + error: (error) => console.error("Error:", error), + complete: () => console.log("Run complete"), + }) +``` + +## Agent Configuration + +Agents accept configuration through the constructor: + +```typescript +interface AgentConfig { + agentId?: string // Unique identifier for the agent + description?: string // Human-readable description + threadId?: string // Conversation thread identifier + initialMessages?: Message[] // Initial messages + initialState?: State // Initial state object +} + +// Using the configuration +const agent = new HttpAgent({ + agentId: "my-agent-123", + description: "A helpful assistant", + threadId: "thread-456", + initialMessages: [ + { id: "1", role: "system", content: "You are a helpful assistant." }, + ], + initialState: { preferredLanguage: "English" }, +}) +``` + +## Agent State Management + +AG-UI agents maintain state across interactions: + +```typescript +// Access current state +console.log(agent.state) + +// Access messages +console.log(agent.messages) + +// Clone an agent with its state +const clonedAgent = agent.clone() +``` + +## Conclusion + +Agents are the foundation of the AG-UI protocol, providing a standardized way to +connect front-end applications with AI services. By implementing the +`AbstractAgent` class, you can create custom integrations with any AI service +while maintaining a consistent interface for your applications. + +The event-driven architecture enables real-time, streaming interactions that are +essential for modern AI applications, and the standardized protocol ensures +compatibility across different implementations. + + +# Core architecture +Source: https://docs.ag-ui.com/concepts/architecture + +Understand how AG-UI connects front-end applications to AI agents + +Agent User Interaction Protocol (AG-UI) is built on a flexible, event-driven +architecture that enables seamless, efficient communication between front-end +applications and AI agents. This document covers the core architectural +components and concepts. + +## Overview + +AG-UI follows a client-server architecture that standardizes communication +between agents and applications: + +```mermaid +flowchart LR + subgraph "Frontend" + App["Application"] + Client["AG-UI Client"] + end + + subgraph "Backend" + A1["AI Agent A"] + P["Secure Proxy"] + A2["AI Agent B"] + A3["AI Agent C"] + end + + App <--> Client + Client <-->|"AG-UI Protocol"| A1 + Client <-->|"AG-UI Protocol"| P + P <-->|"AG-UI Protocol"| A2 + P <-->|"AG-UI Protocol"| A3 + + class P mintStyle; + classDef mintStyle fill:#E0F7E9,stroke:#66BB6A,stroke-width:2px,color:#000000; + + style App rx:5, ry:5; + style Client rx:5, ry:5; + style A1 rx:5, ry:5; + style P rx:5, ry:5; + style A2 rx:5, ry:5; + style A3 rx:5, ry:5; +``` + +* **Application**: User-facing apps (i.e. chat or any AI-enabled application). +* **AG-UI Client**: Generic communication clients like `HttpAgent` or + specialized clients for connecting to existing protocols. +* **Agents**: Backend AI agents that process requests and generate streaming + responses. +* **Secure Proxy**: Backend services that provide additional capabilities and + act as a secure proxy. + +## Core components + +### Protocol layer + +AG-UI's protocol layer provides a flexible foundation for agent communication. + +* **Universal compatibility**: Connect to any protocol by implementing + `run(input: RunAgentInput) -> Observable` + +The protocol's primary abstraction enables applications to run agents and +receive a stream of events: + +{/* prettier-ignore */} + +```typescript +// Core agent execution interface +type RunAgent = () => Observable + +class MyAgent extends AbstractAgent { + run(input: RunAgentInput): RunAgent { + const { threadId, runId } = input + return () => + from([ + { type: EventType.RUN_STARTED, threadId, runId }, + { + type: EventType.MESSAGES_SNAPSHOT, + messages: [ + { id: "msg_1", role: "assistant", content: "Hello, world!" } + ], + }, + { type: EventType.RUN_FINISHED, threadId, runId }, + ]) + } +} +``` + +### Standard HTTP client + +AG-UI offers a standard HTTP client `HttpAgent` that can be used to connect to +any endpoint that accepts POST requests with a body of type `RunAgentInput` and +sends a stream of `BaseEvent` objects. + +`HttpAgent` supports the following transports: + +* **HTTP SSE (Server-Sent Events)** + + * Text-based streaming for wide compatibility + * Easy to read and debug + +* **HTTP binary protocol** + * Highly performant and space-efficient custom transport + * Robust binary serialization for production environments + +### Message types + +AG-UI defines several event categories for different aspects of agent +communication: + +* **Lifecycle events** + + * `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` + * `STEP_STARTED`, `STEP_FINISHED` + +* **Text message events** + + * `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END` + +* **Tool call events** + + * `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END` + +* **State management events** + + * `STATE_SNAPSHOT`, `STATE_DELTA`, `MESSAGES_SNAPSHOT` + +* **Special events** + * `RAW`, `CUSTOM` + +## Running Agents + +To run an agent, you create a client instance and execute it: + +```typescript +// Create an HTTP agent client +const agent = new HttpAgent({ + url: "https://your-agent-endpoint.com/agent", + agentId: "unique-agent-id", + threadId: "conversation-thread" +}); + +// Start the agent and handle events +agent.runAgent({ + tools: [...], + context: [...] +}).subscribe({ + next: (event) => { + // Handle different event types + switch(event.type) { + case EventType.TEXT_MESSAGE_CONTENT: + // Update UI with new content + break; + // Handle other event types + } + }, + error: (error) => console.error("Agent error:", error), + complete: () => console.log("Agent run complete") +}); +``` + +## State Management + +AG-UI provides efficient state management through specialized events: + +* `STATE_SNAPSHOT`: Complete state representation at a point in time +* `STATE_DELTA`: Incremental state changes using JSON Patch format (RFC 6902) +* `MESSAGES_SNAPSHOT`: Complete conversation history + +These events enable efficient client-side state management with minimal data +transfer. + +## Tools and Handoff + +AG-UI supports agent-to-agent handoff and tool usage through standardized +events: + +* Tool definitions are passed in the `runAgent` parameters +* Tool calls are streamed as sequences of `TOOL_CALL_START` → `TOOL_CALL_ARGS` → + `TOOL_CALL_END` events +* Agents can hand off to other agents, maintaining context continuity + +## Events + +All communication in AG-UI is based on typed events. Every event inherits from +`BaseEvent`: + +```typescript +interface BaseEvent { + type: EventType + timestamp?: number + rawEvent?: any +} +``` + +Events are strictly typed and validated, ensuring reliable communication between +components. + + +# Events +Source: https://docs.ag-ui.com/concepts/events + +Understanding events in the Agent User Interaction Protocol + +# Events + +The Agent User Interaction Protocol uses a streaming event-based architecture. +Events are the fundamental units of communication between agents and frontends, +enabling real-time, structured interaction. + +## Event Types Overview + +Events in the protocol are categorized by their purpose: + +| Category | Description | +| ----------------------- | --------------------------------------- | +| Lifecycle Events | Monitor the progression of agent runs | +| Text Message Events | Handle streaming textual content | +| Tool Call Events | Manage tool executions by agents | +| State Management Events | Synchronize state between agents and UI | +| Special Events | Support custom functionality | + +## Base Event Properties + +All events share a common set of base properties: + +| Property | Description | +| ----------- | ---------------------------------------------------------------- | +| `type` | The specific event type identifier | +| `timestamp` | Optional timestamp indicating when the event was created | +| `rawEvent` | Optional field containing the original event data if transformed | + +## Lifecycle Events + +These events represent the lifecycle of an agent run. A typical agent run +follows a predictable pattern: it begins with a `RunStarted` event, may contain +multiple optional `StepStarted`/`StepFinished` pairs, and concludes with either +a `RunFinished` event (success) or a `RunError` event (failure). + +Lifecycle events provide crucial structure to agent runs, enabling frontends to +track progress, manage UI states appropriately, and handle errors gracefully. +They create a consistent framework for understanding when operations begin and +end, making it possible to implement features like loading indicators, progress +tracking, and error recovery mechanisms. + +```mermaid +sequenceDiagram + participant Agent + participant Client + + Note over Agent,Client: Run begins + Agent->>Client: RunStarted + + opt Sending steps is optional + Note over Agent,Client: Step execution + Agent->>Client: StepStarted + Agent->>Client: StepFinished + end + + Note over Agent,Client: Run completes + alt + Agent->>Client: RunFinished + else + Agent->>Client: RunError + end +``` + +The `RunStarted` and either `RunFinished` or `RunError` events are mandatory, +forming the boundaries of an agent run. Step events are optional and may occur +multiple times within a run, allowing for structured, observable progress +tracking. + +### RunStarted + +Signals the start of an agent run. + +The `RunStarted` event is the first event emitted when an agent begins +processing a request. It establishes a new execution context identified by a +unique `runId`. This event serves as a marker for frontends to initialize UI +elements such as progress indicators or loading states. It also provides crucial +identifiers that can be used to associate subsequent events with this specific +run. + +| Property | Description | +| ---------- | ----------------------------- | +| `threadId` | ID of the conversation thread | +| `runId` | ID of the agent run | + +### RunFinished + +Signals the successful completion of an agent run. + +The `RunFinished` event indicates that an agent has successfully completed all +its work for the current run. Upon receiving this event, frontends should +finalize any UI states that were waiting on the agent's completion. This event +marks a clean termination point and indicates that no further processing will +occur in this run unless explicitly requested. The optional `result` field can +contain any output data produced by the agent run. + +| Property | Description | +| ---------- | ----------------------------- | +| `threadId` | ID of the conversation thread | +| `runId` | ID of the agent run | +| `result` | Optional result data from run | + +### RunError + +Signals an error during an agent run. + +The `RunError` event indicates that the agent encountered an error it could not +recover from, causing the run to terminate prematurely. This event provides +information about what went wrong, allowing frontends to display appropriate +error messages and potentially offer recovery options. After a `RunError` event, +no further processing will occur in this run. + +| Property | Description | +| --------- | ------------------- | +| `message` | Error message | +| `code` | Optional error code | + +### StepStarted + +Signals the start of a step within an agent run. + +The `StepStarted` event indicates that the agent is beginning a specific subtask +or phase of its processing. Steps provide granular visibility into the agent's +progress, enabling more precise tracking and feedback in the UI. Steps are +optional but highly recommended for complex operations that benefit from being +broken down into observable stages. The `stepName` could be the name of a node +or function that is currently executing. + +| Property | Description | +| ---------- | ---------------- | +| `stepName` | Name of the step | + +### StepFinished + +Signals the completion of a step within an agent run. + +The `StepFinished` event indicates that the agent has completed a specific +subtask or phase. When paired with a corresponding `StepStarted` event, it +creates a bounded context for a discrete unit of work. Frontends can use these +events to update progress indicators, show completion animations, or reveal +results specific to that step. The `stepName` must match the corresponding +`StepStarted` event to properly pair the beginning and end of the step. + +| Property | Description | +| ---------- | ---------------- | +| `stepName` | Name of the step | + +## Text Message Events + +These events represent the lifecycle of text messages in a conversation. Text +message events follow a streaming pattern, where content is delivered +incrementally. A message begins with a `TextMessageStart` event, followed by one +or more `TextMessageContent` events that deliver chunks of text as they become +available, and concludes with a `TextMessageEnd` event. + +This streaming approach enables real-time display of message content as it's +generated, creating a more responsive user experience compared to waiting for +the entire message to be complete before showing anything. + +```mermaid +sequenceDiagram + participant Agent + participant Client + + Note over Agent,Client: Message begins + Agent->>Client: TextMessageStart + + loop Content streaming + Agent->>Client: TextMessageContent + end + + Note over Agent,Client: Message completes + Agent->>Client: TextMessageEnd +``` + +The `TextMessageContent` events each contain a `delta` field with a chunk of +text. Frontends should concatenate these deltas in the order received to +construct the complete message. The `messageId` property links all related +events, allowing the frontend to associate content chunks with the correct +message. + +### TextMessageStart + +Signals the start of a text message. + +The `TextMessageStart` event initializes a new text message in the conversation. +It establishes a unique `messageId` that will be referenced by subsequent +content chunks and the end event. This event allows frontends to prepare the UI +for an incoming message, such as creating a new message bubble with a loading +indicator. The `role` property identifies whether the message is coming from the +assistant or potentially another participant in the conversation. + +| Property | Description | +| ----------- | ---------------------------------------------- | +| `messageId` | Unique identifier for the message | +| `role` | Role of the message sender (e.g., "assistant") | + +### TextMessageContent + +Represents a chunk of content in a streaming text message. + +The `TextMessageContent` event delivers incremental parts of the message text as +they become available. Each event contains a small chunk of text in the `delta` +property that should be appended to previously received chunks. The streaming +nature of these events enables real-time display of content, creating a more +responsive and engaging user experience. Implementations should handle these +events efficiently to ensure smooth text rendering without visible delays or +flickering. + +| Property | Description | +| ----------- | -------------------------------------- | +| `messageId` | Matches the ID from `TextMessageStart` | +| `delta` | Text content chunk (non-empty) | + +### TextMessageEnd + +Signals the end of a text message. + +The `TextMessageEnd` event marks the completion of a streaming text message. +After receiving this event, the frontend knows that the message is complete and +no further content will be added. This allows the UI to finalize rendering, +remove any loading indicators, and potentially trigger actions that should occur +after message completion, such as enabling reply controls or performing +automatic scrolling to ensure the full message is visible. + +| Property | Description | +| ----------- | -------------------------------------- | +| `messageId` | Matches the ID from `TextMessageStart` | + +## Tool Call Events + +These events represent the lifecycle of tool calls made by agents. Tool calls +follow a streaming pattern similar to text messages. When an agent needs to use +a tool, it emits a `ToolCallStart` event, followed by one or more `ToolCallArgs` +events that stream the arguments being passed to the tool, and concludes with a +`ToolCallEnd` event. + +This streaming approach allows frontends to show tool executions in real-time, +making the agent's actions transparent and providing immediate feedback about +what tools are being invoked and with what parameters. + +```mermaid +sequenceDiagram + participant Agent + participant Client + + Note over Agent,Client: Tool call begins + Agent->>Client: ToolCallStart + + loop Arguments streaming + Agent->>Client: ToolCallArgs + end + + Note over Agent,Client: Tool call completes + Agent->>Client: ToolCallEnd + + Note over Agent,Client: Tool execution result + Agent->>Client: ToolCallResult +``` + +The `ToolCallArgs` events each contain a `delta` field with a chunk of the +arguments. Frontends should concatenate these deltas in the order received to +construct the complete arguments object. The `toolCallId` property links all +related events, allowing the frontend to associate argument chunks with the +correct tool call. + +### ToolCallStart + +Signals the start of a tool call. + +The `ToolCallStart` event indicates that the agent is invoking a tool to perform +a specific function. This event provides the name of the tool being called and +establishes a unique `toolCallId` that will be referenced by subsequent events +in this tool call. Frontends can use this event to display tool usage to users, +such as showing a notification that a specific operation is in progress. The +optional `parentMessageId` allows linking the tool call to a specific message in +the conversation, providing context for why the tool is being used. + +| Property | Description | +| ----------------- | ----------------------------------- | +| `toolCallId` | Unique identifier for the tool call | +| `toolCallName` | Name of the tool being called | +| `parentMessageId` | Optional ID of the parent message | + +### ToolCallArgs + +Represents a chunk of argument data for a tool call. + +The `ToolCallArgs` event delivers incremental parts of the tool's arguments as +they become available. Each event contains a segment of the argument data in the +`delta` property. These deltas are often JSON fragments that, when combined, +form the complete arguments object for the tool. Streaming the arguments is +particularly valuable for complex tool calls where constructing the full +arguments may take time. Frontends can progressively reveal these arguments to +users, providing insight into exactly what parameters are being passed to tools. + +| Property | Description | +| ------------ | ----------------------------------- | +| `toolCallId` | Matches the ID from `ToolCallStart` | +| `delta` | Argument data chunk | + +### ToolCallEnd + +Signals the end of a tool call. + +The `ToolCallEnd` event marks the completion of a tool call. After receiving +this event, the frontend knows that all arguments have been transmitted and the +tool execution is underway or completed. This allows the UI to finalize the tool +call display and prepare for potential results. In systems where tool execution +results are returned separately, this event indicates that the agent has +finished specifying the tool and its arguments, and is now waiting for or has +received the results. + +| Property | Description | +| ------------ | ----------------------------------- | +| `toolCallId` | Matches the ID from `ToolCallStart` | + +### ToolCallResult + +Provides the result of a tool call execution. + +The `ToolCallResult` event delivers the output or result from a tool that was +previously invoked by the agent. This event is sent after the tool has been +executed by the system and contains the actual output generated by the tool. +Unlike the streaming pattern of tool call specification (start, args, end), the +result is delivered as a complete unit since tool execution typically produces a +complete output. Frontends can use this event to display tool results to users, +append them to the conversation history, or trigger follow-up actions based on +the tool's output. + +| Property | Description | +| ------------ | ----------------------------------------------------------- | +| `messageId` | ID of the conversation message this result belongs to | +| `toolCallId` | Matches the ID from the corresponding `ToolCallStart` event | +| `content` | The actual result/output content from the tool execution | +| `role` | Optional role identifier, typically "tool" for tool results | + +## State Management Events + +These events are used to manage and synchronize the agent's state with the +frontend. State management in the protocol follows an efficient snapshot-delta +pattern where complete state snapshots are sent initially or infrequently, while +incremental updates (deltas) are used for ongoing changes. + +This approach optimizes for both completeness and efficiency: snapshots ensure +the frontend has the full state context, while deltas minimize data transfer for +frequent updates. Together, they enable frontends to maintain an accurate +representation of agent state without unnecessary data transmission. + +```mermaid +sequenceDiagram + participant Agent + participant Client + + Note over Agent,Client: Initial state transfer + Agent->>Client: StateSnapshot + + Note over Agent,Client: Incremental updates + loop State changes over time + Agent->>Client: StateDelta + Agent->>Client: StateDelta + end + + Note over Agent,Client: Occasional full refresh + Agent->>Client: StateSnapshot + + loop More incremental updates + Agent->>Client: StateDelta + end + + Note over Agent,Client: Message history update + Agent->>Client: MessagesSnapshot +``` + +The combination of snapshots and deltas allows frontends to efficiently track +changes to agent state while ensuring consistency. Snapshots serve as +synchronization points that reset the state to a known baseline, while deltas +provide lightweight updates between snapshots. + +### StateSnapshot + +Provides a complete snapshot of an agent's state. + +The `StateSnapshot` event delivers a comprehensive representation of the agent's +current state. This event is typically sent at the beginning of an interaction +or when synchronization is needed. It contains all state variables relevant to +the frontend, allowing it to completely rebuild its internal representation. +Frontends should replace their existing state model with the contents of this +snapshot rather than trying to merge it with previous state. + +| Property | Description | +| ---------- | ----------------------- | +| `snapshot` | Complete state snapshot | + +### StateDelta + +Provides a partial update to an agent's state using JSON Patch. + +The `StateDelta` event contains incremental updates to the agent's state in the +form of JSON Patch operations (as defined in RFC 6902). Each delta represents +specific changes to apply to the current state model. This approach is +bandwidth-efficient, sending only what has changed rather than the entire state. +Frontends should apply these patches in sequence to maintain an accurate state +representation. If a frontend detects inconsistencies after applying patches, it +may request a fresh `StateSnapshot`. + +| Property | Description | +| -------- | ----------------------------------------- | +| `delta` | Array of JSON Patch operations (RFC 6902) | + +### MessagesSnapshot + +Provides a snapshot of all messages in a conversation. + +The `MessagesSnapshot` event delivers a complete history of messages in the +current conversation. Unlike the general state snapshot, this focuses +specifically on the conversation transcript. This event is useful for +initializing the chat history, synchronizing after connection interruptions, or +providing a comprehensive view when a user joins an ongoing conversation. +Frontends should use this to establish or refresh the conversational context +displayed to users. + +| Property | Description | +| ---------- | ------------------------ | +| `messages` | Array of message objects | + +## Special Events + +Special events provide flexibility in the protocol by allowing for +system-specific functionality and integration with external systems. These +events don't follow the standard lifecycle or streaming patterns of other event +types but instead serve specialized purposes. + +### Raw + +Used to pass through events from external systems. + +The `Raw` event acts as a container for events originating from external systems +or sources that don't natively follow the Agent UI Protocol. This event type +enables interoperability with other event-based systems by wrapping their events +in a standardized format. The enclosed event data is preserved in its original +form inside the `event` property, while the optional `source` property +identifies the system it came from. Frontends can use this information to handle +external events appropriately, either by processing them directly or by +delegating them to system-specific handlers. + +| Property | Description | +| -------- | -------------------------- | +| `event` | Original event data | +| `source` | Optional source identifier | + +### Custom + +Used for application-specific custom events. + +The `Custom` event provides an extension mechanism for implementing features not +covered by the standard event types. Unlike `Raw` events which act as +passthrough containers, `Custom` events are explicitly part of the protocol but +with application-defined semantics. The `name` property identifies the specific +custom event type, while the `value` property contains the associated data. This +mechanism allows for protocol extensions without requiring formal specification +changes. Teams should document their custom events to ensure consistent +implementation across frontends and agents. + +| Property | Description | +| -------- | ------------------------------- | +| `name` | Name of the custom event | +| `value` | Value associated with the event | + +## Event Flow Patterns + +Events in the protocol typically follow specific patterns: + +1. **Start-Content-End Pattern**: Used for streaming content (text messages, + tool calls) + + * `Start` event initiates the stream + * `Content` events deliver data chunks + * `End` event signals completion + +2. **Snapshot-Delta Pattern**: Used for state synchronization + + * `Snapshot` provides complete state + * `Delta` events provide incremental updates + +3. **Lifecycle Pattern**: Used for monitoring agent runs + * `Started` events signal beginnings + * `Finished`/`Error` events signal endings + +## Implementation Considerations + +When implementing event handlers: + +* Events should be processed in the order they are received +* Events with the same ID (e.g., `messageId`, `toolCallId`) belong to the same + logical stream +* Implementations should be resilient to out-of-order delivery +* Custom events should follow the established patterns for consistency + + +# Messages +Source: https://docs.ag-ui.com/concepts/messages + +Understanding message structure and communication in AG-UI + +# Messages + +Messages form the backbone of communication in the AG-UI protocol. They +represent the conversation history between users and AI agents, and provide a +standardized way to exchange information regardless of the underlying AI service +being used. + +## Message Structure + +AG-UI messages follow a vendor-neutral format, ensuring compatibility across +different AI providers while maintaining a consistent structure. This allows +applications to switch between AI services (like OpenAI, Anthropic, or custom +models) without changing the client-side implementation. + +The basic message structure includes: + +```typescript +interface BaseMessage { + id: string // Unique identifier for the message + role: string // The role of the sender (user, assistant, system, tool) + content?: string // Optional text content of the message + name?: string // Optional name of the sender +} +``` + +## Message Types + +AG-UI supports several message types to accommodate different participants in a +conversation: + +### User Messages + +Messages from the end user to the agent: + +```typescript +interface UserMessage { + id: string + role: "user" + content: string // Text input from the user + name?: string // Optional user identifier +} +``` + +### Assistant Messages + +Messages from the AI assistant to the user: + +```typescript +interface AssistantMessage { + id: string + role: "assistant" + content?: string // Text response from the assistant (optional if using tool calls) + name?: string // Optional assistant identifier + toolCalls?: ToolCall[] // Optional tool calls made by the assistant +} +``` + +### System Messages + +Instructions or context provided to the agent: + +```typescript +interface SystemMessage { + id: string + role: "system" + content: string // Instructions or context for the agent + name?: string // Optional identifier +} +``` + +### Tool Messages + +Results from tool executions: + +```typescript +interface ToolMessage { + id: string + role: "tool" + content: string // Result from the tool execution + toolCallId: string // ID of the tool call this message responds to +} +``` + +### Developer Messages + +Internal messages used for development or debugging: + +```typescript +interface DeveloperMessage { + id: string + role: "developer" + content: string + name?: string +} +``` + +## Vendor Neutrality + +AG-UI messages are designed to be vendor-neutral, meaning they can be easily +mapped to and from proprietary formats used by various AI providers: + +```typescript +// Example: Converting AG-UI messages to OpenAI format +const openaiMessages = agUiMessages + .filter((msg) => ["user", "system", "assistant"].includes(msg.role)) + .map((msg) => ({ + role: msg.role as "user" | "system" | "assistant", + content: msg.content || "", + // Map tool calls if present + ...(msg.role === "assistant" && msg.toolCalls + ? { + tool_calls: msg.toolCalls.map((tc) => ({ + id: tc.id, + type: tc.type, + function: { + name: tc.function.name, + arguments: tc.function.arguments, + }, + })), + } + : {}), + })) +``` + +This abstraction allows AG-UI to serve as a common interface regardless of the +underlying AI service. + +## Message Synchronization + +Messages can be synchronized between client and server through two primary +mechanisms: + +### Complete Snapshots + +The `MESSAGES_SNAPSHOT` event provides a complete view of all messages in a +conversation: + +```typescript +interface MessagesSnapshotEvent { + type: EventType.MESSAGES_SNAPSHOT + messages: Message[] // Complete array of all messages +} +``` + +This is typically used: + +* When initializing a conversation +* After connection interruptions +* When major state changes occur +* To ensure client-server synchronization + +### Streaming Messages + +For real-time interactions, new messages can be streamed as they're generated: + +1. **Start a message**: Indicate a new message is being created + + ```typescript + interface TextMessageStartEvent { + type: EventType.TEXT_MESSAGE_START + messageId: string + role: string + } + ``` + +2. **Stream content**: Send content chunks as they become available + + ```typescript + interface TextMessageContentEvent { + type: EventType.TEXT_MESSAGE_CONTENT + messageId: string + delta: string // Text chunk to append + } + ``` + +3. **End a message**: Signal the message is complete + ```typescript + interface TextMessageEndEvent { + type: EventType.TEXT_MESSAGE_END + messageId: string + } + ``` + +This streaming approach provides a responsive user experience with immediate +feedback. + +## Tool Integration in Messages + +AG-UI messages elegantly integrate tool usage, allowing agents to perform +actions and process their results: + +### Tool Calls + +Tool calls are embedded within assistant messages: + +```typescript +interface ToolCall { + id: string // Unique ID for this tool call + type: "function" // Type of tool call + function: { + name: string // Name of the function to call + arguments: string // JSON-encoded string of arguments + } +} +``` + +Example assistant message with tool calls: + +```typescript +{ + id: "msg_123", + role: "assistant", + content: "I'll help you with that calculation.", + toolCalls: [ + { + id: "call_456", + type: "function", + function: { + name: "calculate", + arguments: '{"expression": "24 * 7"}' + } + } + ] +} +``` + +### Tool Results + +Results from tool executions are represented as tool messages: + +```typescript +{ + id: "result_789", + role: "tool", + content: "168", + toolCallId: "call_456" // References the original tool call +} +``` + +This creates a clear chain of tool usage: + +1. Assistant requests a tool call +2. Tool executes and returns a result +3. Assistant can reference and respond to the result + +## Streaming Tool Calls + +Similar to text messages, tool calls can be streamed to provide real-time +visibility into the agent's actions: + +1. **Start a tool call**: + + ```typescript + interface ToolCallStartEvent { + type: EventType.TOOL_CALL_START + toolCallId: string + toolCallName: string + parentMessageId?: string // Optional link to parent message + } + ``` + +2. **Stream arguments**: + + ```typescript + interface ToolCallArgsEvent { + type: EventType.TOOL_CALL_ARGS + toolCallId: string + delta: string // JSON fragment to append to arguments + } + ``` + +3. **End a tool call**: + ```typescript + interface ToolCallEndEvent { + type: EventType.TOOL_CALL_END + toolCallId: string + } + ``` + +This allows frontends to show tools being invoked progressively as the agent +constructs its reasoning. + +## Practical Example + +Here's a complete example of a conversation with tool usage: + +```typescript +// Conversation history +;[ + // User query + { + id: "msg_1", + role: "user", + content: "What's the weather in New York?", + }, + + // Assistant response with tool call + { + id: "msg_2", + role: "assistant", + content: "Let me check the weather for you.", + toolCalls: [ + { + id: "call_1", + type: "function", + function: { + name: "get_weather", + arguments: '{"location": "New York", "unit": "celsius"}', + }, + }, + ], + }, + + // Tool result + { + id: "result_1", + role: "tool", + content: + '{"temperature": 22, "condition": "Partly Cloudy", "humidity": 65}', + toolCallId: "call_1", + }, + + // Assistant's final response using tool results + { + id: "msg_3", + role: "assistant", + content: + "The weather in New York is partly cloudy with a temperature of 22°C and 65% humidity.", + }, +] +``` + +## Conclusion + +The message structure in AG-UI enables sophisticated conversational AI +experiences while maintaining vendor neutrality. By standardizing how messages +are represented, synchronized, and streamed, AG-UI provides a consistent way to +implement interactive human-agent communication regardless of the underlying AI +service. + +This system supports everything from simple text exchanges to complex tool-based +workflows, all while optimizing for both real-time responsiveness and efficient +data transfer. + + +# State Management +Source: https://docs.ag-ui.com/concepts/state + +Understanding state synchronization between agents and frontends in AG-UI + +# State Management + +State management is a core feature of the AG-UI protocol that enables real-time +synchronization between agents and frontend applications. By providing efficient +mechanisms for sharing and updating state, AG-UI creates a foundation for +collaborative experiences where both AI agents and human users can work together +seamlessly. + +## Shared State Architecture + +In AG-UI, state is a structured data object that: + +1. Persists across interactions with an agent +2. Can be accessed by both the agent and the frontend +3. Updates in real-time as the interaction progresses +4. Provides context for decision-making on both sides + +This shared state architecture creates a bidirectional communication channel +where: + +* Agents can access the application's current state to make informed decisions +* Frontends can observe and react to changes in the agent's internal state +* Both sides can modify the state, creating a collaborative workflow + +## State Synchronization Methods + +AG-UI provides two complementary methods for state synchronization: + +### State Snapshots + +The `STATE_SNAPSHOT` event delivers a complete representation of an agent's +current state: + +```typescript +interface StateSnapshotEvent { + type: EventType.STATE_SNAPSHOT + snapshot: any // Complete state object +} +``` + +Snapshots are typically used: + +* At the beginning of an interaction to establish the initial state +* After connection interruptions to ensure synchronization +* When major state changes occur that require a complete refresh +* To establish a new baseline for future delta updates + +When a frontend receives a `STATE_SNAPSHOT` event, it should replace its +existing state model entirely with the contents of the snapshot. + +### State Deltas + +The `STATE_DELTA` event delivers incremental updates to the state using JSON +Patch format (RFC 6902): + +```typescript +interface StateDeltaEvent { + type: EventType.STATE_DELTA + delta: JsonPatchOperation[] // Array of JSON Patch operations +} +``` + +Deltas are bandwidth-efficient, sending only what has changed rather than the +entire state. This approach is particularly valuable for: + +* Frequent small updates during streaming interactions +* Large state objects where most properties remain unchanged +* High-frequency updates that would be inefficient to send as full snapshots + +## JSON Patch Format + +AG-UI uses the JSON Patch format (RFC 6902) for state deltas, which defines a +standardized way to express changes to a JSON document: + +```typescript +interface JsonPatchOperation { + op: "add" | "remove" | "replace" | "move" | "copy" | "test" + path: string // JSON Pointer (RFC 6901) to the target location + value?: any // The value to apply (for add, replace) + from?: string // Source path (for move, copy) +} +``` + +Common operations include: + +1. **add**: Adds a value to an object or array + + ```json + { "op": "add", "path": "/user/preferences", "value": { "theme": "dark" } } + ``` + +2. **replace**: Replaces a value + + ```json + { "op": "replace", "path": "/conversation_state", "value": "paused" } + ``` + +3. **remove**: Removes a value + + ```json + { "op": "remove", "path": "/temporary_data" } + ``` + +4. **move**: Moves a value from one location to another + ```json + { "op": "move", "path": "/completed_items", "from": "/pending_items/0" } + ``` + +Frontends should apply these patches in sequence to maintain an accurate state +representation. If inconsistencies are detected after applying patches, the +frontend can request a fresh `STATE_SNAPSHOT`. + +## State Processing in AG-UI + +In the AG-UI implementation, state deltas are applied using the +`fast-json-patch` library: + +```typescript +case EventType.STATE_DELTA: { + const { delta } = event as StateDeltaEvent; + + try { + // Apply the JSON Patch operations to the current state without mutating the original + const result = applyPatch(state, delta, true, false); + state = result.newDocument; + return emitUpdate({ state }); + } catch (error: unknown) { + console.warn( + `Failed to apply state patch:\n` + + `Current state: ${JSON.stringify(state, null, 2)}\n` + + `Patch operations: ${JSON.stringify(delta, null, 2)}\n` + + `Error: ${errorMessage}` + ); + return emitNoUpdate(); + } +} +``` + +This implementation ensures that: + +* Patches are applied atomically (all or none) +* The original state is not mutated during the application process +* Errors are caught and handled gracefully + +## Human-in-the-Loop Collaboration + +The shared state system is fundamental to human-in-the-loop workflows in AG-UI. +It enables: + +1. **Real-time visibility**: Users can observe the agent's thought process and + current status +2. **Contextual awareness**: The agent can access user actions, preferences, and + application state +3. **Collaborative decision-making**: Both human and AI can contribute to the + evolving state +4. **Feedback loops**: Humans can correct or guide the agent by modifying state + properties + +For example, an agent might update its state with a proposed action: + +```json +{ + "proposal": { + "action": "send_email", + "recipient": "client@example.com", + "content": "Draft email content..." + } +} +``` + +The frontend can display this proposal to the user, who can then approve, +reject, or modify it before execution. + +## CopilotKit Implementation + +[CopilotKit](https://docs.copilotkit.ai), a popular framework for building AI +assistants, leverages AG-UI's state management system through its "shared state" +feature. This implementation enables bidirectional state synchronization between +agents (particularly LangGraph agents) and frontend applications. + +CopilotKit's shared state system is implemented through: + +```jsx +// In the frontend React application +const { state: agentState, setState: setAgentState } = useCoAgent({ + name: "agent", + initialState: { someProperty: "initialValue" }, +}) +``` + +This hook creates a real-time connection to the agent's state, allowing: + +1. Reading the agent's current state in the frontend +2. Updating the agent's state from the frontend +3. Rendering UI components based on the agent's state + +On the backend, LangGraph agents can emit state updates using: + +```python +# In the LangGraph agent +async def tool_node(self, state: ResearchState, config: RunnableConfig): + # Update state with new information + tool_state = { + "title": new_state.get("title", ""), + "outline": new_state.get("outline", {}), + "sections": new_state.get("sections", []), + # Other state properties... + } + + # Emit updated state to frontend + await copilotkit_emit_state(config, tool_state) + + return tool_state +``` + +These state updates are transmitted using AG-UI's state snapshot and delta +mechanisms, creating a seamless shared context between agent and frontend. + +## Best Practices + +When implementing state management in AG-UI: + +1. **Use snapshots judiciously**: Full snapshots should be sent only when + necessary to establish a baseline. +2. **Prefer deltas for incremental changes**: Small state updates should use + deltas to minimize data transfer. +3. **Structure state thoughtfully**: Design state objects to support partial + updates and minimize patch complexity. +4. **Handle state conflicts**: Implement strategies for resolving conflicting + updates from agent and frontend. +5. **Include error recovery**: Provide mechanisms to resynchronize state if + inconsistencies are detected. +6. **Consider security implications**: Avoid storing sensitive information in + shared state. + +## Conclusion + +AG-UI's state management system provides a powerful foundation for building +collaborative applications where humans and AI agents work together. By +efficiently synchronizing state between frontend and backend through snapshots +and JSON Patch deltas, AG-UI enables sophisticated human-in-the-loop workflows +that combine the strengths of both human intuition and AI capabilities. + +The implementation in frameworks like CopilotKit demonstrates how this shared +state approach can create collaborative experiences that are more effective than +either fully autonomous systems or traditional user interfaces. + + +# Tools +Source: https://docs.ag-ui.com/concepts/tools + +Understanding tools and how they enable human-in-the-loop AI workflows + +# Tools + +Tools are a fundamental concept in the AG-UI protocol that enable AI agents to +interact with external systems and incorporate human judgment into their +workflows. By defining tools in the frontend and passing them to agents, +developers can create sophisticated human-in-the-loop experiences that combine +AI capabilities with human expertise. + +## What Are Tools? + +In AG-UI, tools are functions that agents can call to: + +1. Request specific information +2. Perform actions in external systems +3. Ask for human input or confirmation +4. Access specialized capabilities + +Tools bridge the gap between AI reasoning and real-world actions, allowing +agents to accomplish tasks that would be impossible through conversation alone. + +## Tool Structure + +Tools follow a consistent structure that defines their name, purpose, and +expected parameters: + +```typescript +interface Tool { + name: string // Unique identifier for the tool + description: string // Human-readable explanation of what the tool does + parameters: { + // JSON Schema defining the tool's parameters + type: "object" + properties: { + // Tool-specific parameters + } + required: string[] // Array of required parameter names + } +} +``` + +The `parameters` field uses [JSON Schema](https://json-schema.org/) to define +the structure of arguments that the tool accepts. This schema is used by both +the agent (to generate valid tool calls) and the frontend (to validate and parse +tool arguments). + +## Frontend-Defined Tools + +A key aspect of AG-UI's tool system is that tools are defined in the frontend +and passed to the agent during execution: + +```typescript +// Define tools in the frontend +const userConfirmationTool = { + name: "confirmAction", + description: "Ask the user to confirm a specific action before proceeding", + parameters: { + type: "object", + properties: { + action: { + type: "string", + description: "The action that needs user confirmation", + }, + importance: { + type: "string", + enum: ["low", "medium", "high", "critical"], + description: "The importance level of the action", + }, + }, + required: ["action"], + }, +} + +// Pass tools to the agent during execution +agent.runAgent({ + tools: [userConfirmationTool], + // Other parameters... +}) +``` + +This approach has several advantages: + +1. **Frontend control**: The frontend determines what capabilities are available + to the agent +2. **Dynamic capabilities**: Tools can be added or removed based on user + permissions, context, or application state +3. **Separation of concerns**: Agents focus on reasoning while frontends handle + tool implementation +4. **Security**: Sensitive operations are controlled by the application, not the + agent + +## Tool Call Lifecycle + +When an agent needs to use a tool, it follows a standardized sequence of events: + +1. **ToolCallStart**: Indicates the beginning of a tool call with a unique ID + and tool name + + ```typescript + { + type: EventType.TOOL_CALL_START, + toolCallId: "tool-123", + toolCallName: "confirmAction", + parentMessageId: "msg-456" // Optional reference to a message + } + ``` + +2. **ToolCallArgs**: Streams the tool arguments as they're generated + + ```typescript + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "tool-123", + delta: '{"act' // Partial JSON being streamed + } + ``` + + ```typescript + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "tool-123", + delta: 'ion":"Depl' // More JSON being streamed + } + ``` + + ```typescript + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "tool-123", + delta: 'oy the application to production"}' // Final JSON fragment + } + ``` + +3. **ToolCallEnd**: Marks the completion of the tool call + ```typescript + { + type: EventType.TOOL_CALL_END, + toolCallId: "tool-123" + } + ``` + +The frontend accumulates these deltas to construct the complete tool call +arguments. Once the tool call is complete, the frontend can execute the tool and +provide results back to the agent. + +## Tool Results + +After a tool has been executed, the result is sent back to the agent as a "tool +message": + +```typescript +{ + id: "result-789", + role: "tool", + content: "true", // Tool result as a string + toolCallId: "tool-123" // References the original tool call +} +``` + +This message becomes part of the conversation history, allowing the agent to +reference and incorporate the tool's result in subsequent responses. + +## Human-in-the-Loop Workflows + +The AG-UI tool system is especially powerful for implementing human-in-the-loop +workflows. By defining tools that request human input or confirmation, +developers can create AI experiences that seamlessly blend autonomous operation +with human judgment. + +For example: + +1. Agent needs to make an important decision +2. Agent calls the `confirmAction` tool with details about the decision +3. Frontend displays a confirmation dialog to the user +4. User provides their input +5. Frontend sends the user's decision back to the agent +6. Agent continues processing with awareness of the user's choice + +This pattern enables use cases like: + +* **Approval workflows**: AI suggests actions that require human approval +* **Data verification**: Humans verify or correct AI-generated data +* **Collaborative decision-making**: AI and humans jointly solve complex + problems +* **Supervised learning**: Human feedback improves future AI decisions + +## CopilotKit Integration + +[CopilotKit](https://docs.copilotkit.ai/) provides a simplified way to work with +AG-UI tools in React applications through its +[`useCopilotAction`](https://docs.copilotkit.ai/guides/frontend-actions) hook: + +```tsx +import { useCopilotAction } from "@copilotkit/react-core" + +// Define a tool for user confirmation +useCopilotAction({ + name: "confirmAction", + description: "Ask the user to confirm an action", + parameters: { + type: "object", + properties: { + action: { + type: "string", + description: "The action to confirm", + }, + }, + required: ["action"], + }, + handler: async ({ action }) => { + // Show a confirmation dialog + const confirmed = await showConfirmDialog(action) + return confirmed ? "approved" : "rejected" + }, +}) +``` + +This approach makes it easy to define tools that integrate with your React +components and handle the tool execution logic in a clean, declarative way. + +## Tool Examples + +Here are some common types of tools used in AG-UI applications: + +### User Confirmation + +```typescript +{ + name: "confirmAction", + description: "Ask the user to confirm an action", + parameters: { + type: "object", + properties: { + action: { + type: "string", + description: "The action to confirm" + }, + importance: { + type: "string", + enum: ["low", "medium", "high", "critical"], + description: "The importance level" + } + }, + required: ["action"] + } +} +``` + +### Data Retrieval + +```typescript +{ + name: "fetchUserData", + description: "Retrieve data about a specific user", + parameters: { + type: "object", + properties: { + userId: { + type: "string", + description: "ID of the user" + }, + fields: { + type: "array", + items: { + type: "string" + }, + description: "Fields to retrieve" + } + }, + required: ["userId"] + } +} +``` + +### User Interface Control + +```typescript +{ + name: "navigateTo", + description: "Navigate to a different page or view", + parameters: { + type: "object", + properties: { + destination: { + type: "string", + description: "Destination page or view" + }, + params: { + type: "object", + description: "Optional parameters for the navigation" + } + }, + required: ["destination"] + } +} +``` + +### Content Generation + +```typescript +{ + name: "generateImage", + description: "Generate an image based on a description", + parameters: { + type: "object", + properties: { + prompt: { + type: "string", + description: "Description of the image to generate" + }, + style: { + type: "string", + description: "Visual style for the image" + }, + dimensions: { + type: "object", + properties: { + width: { type: "number" }, + height: { type: "number" } + }, + description: "Dimensions of the image" + } + }, + required: ["prompt"] + } +} +``` + +## Best Practices + +When designing tools for AG-UI: + +1. **Clear naming**: Use descriptive, action-oriented names +2. **Detailed descriptions**: Include thorough descriptions to help the agent + understand when and how to use the tool +3. **Structured parameters**: Define precise parameter schemas with descriptive + field names and constraints +4. **Required fields**: Only mark parameters as required if they're truly + necessary +5. **Error handling**: Implement robust error handling in tool execution code +6. **User experience**: Design tool UIs that provide appropriate context for + human decision-making + +## Conclusion + +Tools in AG-UI bridge the gap between AI reasoning and real-world actions, +enabling sophisticated workflows that combine the strengths of AI and human +intelligence. By defining tools in the frontend and passing them to agents, +developers can create interactive experiences where AI and humans collaborate +efficiently. + +The tool system is particularly powerful for implementing human-in-the-loop +workflows, where AI can suggest actions but defer critical decisions to humans. +This balances automation with human judgment, creating AI experiences that are +both powerful and trustworthy. + + +# Contributing +Source: https://docs.ag-ui.com/development/contributing + +How to participate in Agent User Interaction Protocol development + +# Naming conventions + +Add your package under `typescript-sdk/integrations/` with docs and tests. + +If your integration is work in progress, you can still add it to main branch. +You can prefix it with `wip-`, i.e. +(`typescript-sdk/integrations/wip-your-integration`) or if you're a third party +contributor use the `community` prefix, i.e. +(`typescript-sdk/integrations/community-your-integration`). + +For questions and discussions, please use +[GitHub Discussions](https://github.com/orgs/ag-ui-protocol/discussions). + + +# Roadmap +Source: https://docs.ag-ui.com/development/roadmap + +Our plans for evolving Agent User Interaction Protocol + +The Agent User Interaction Protocol is rapidly evolving. This page outlines our +current thinking on key priorities and future direction. + + + The ideas presented here are not commitments—we may solve these challenges + differently than described, or some may not materialize at all. This is also + not an *exhaustive* list; we may incorporate work that isn't mentioned here. + + +## Get Involved + +We welcome community participation in shaping AG-UI's future. Visit our +[GitHub Discussions](https://github.com/orgs/ag-ui-protocol/discussions) to join +the conversation and contribute your ideas. + + +# What's New +Source: https://docs.ag-ui.com/development/updates + +The latest updates and improvements to AG-UI + + + * Initial release of the Agent User Interaction Protocol + + + +# Introduction +Source: https://docs.ag-ui.com/introduction + +Get started with the Agent User Interaction Protocol (AG-UI) + +**AG-UI** standardizes how **front-end applications connect to AI agents** +through an open protocol. Think of it as a universal translator for AI-driven +systems- no matter what language an agent speaks: **AG-UI ensures fluent +communication**. + +## Why AG-UI? + +AG-UI helps developers build next-generation AI workflows that need **real-time +interactivity**, **live state streaming** and **human-in-the-loop +collaboration**. + +AG-UI provides: + +* **A straightforward approach** to integrating AI agents with the front-end + through frameworks such as + [CopilotKit 🪁](https://github.com/CopilotKit/CopilotKit) +* **Building blocks** for an efficient wire protocol for human⚡️agent + communication +* **Best practices** for chat, streaming state updates, human-in-the-loop and + shared state + +## Existing Integrations + +AG-UI has been integrated with several popular agent frameworks, making it easy +to adopt regardless of your preferred tooling: + +* **[LangGraph](https://docs.copilotkit.ai/coagents)**: Build agent-native + applications with shared state and human-in-the-loop workflows using + LangGraph's powerful orchestration capabilities. +* **[CrewAI Flows](https://docs.copilotkit.ai/crewai-flows)**: Create sequential + multi-agent workflows with well-defined stages and process control. +* **[CrewAI Crews](https://docs.copilotkit.ai/crewai-crews)**: Design + collaborative agent teams with specialized roles and inter-agent + communication. +* **[Mastra](/mastra)**: Leverage TypeScript for building strongly-typed agent + implementations with enhanced developer experience. +* **[AG2](/ag2)**: Utilize the open-source AgentOS for scalable, + production-ready agent deployments. + +These integrations make it straightforward to connect your preferred agent +framework with frontend applications through the AG-UI protocol. + +### Architecture + +At its core, AG-UI bridges AI agents and front-end applications using a +lightweight, event-driven protocol: + +```mermaid +flowchart LR + subgraph "Frontend" + FE["Front-end"] + end + + subgraph "Backend" + A1["AI Agent A"] + P["Secure Proxy"] + A2["AI Agent B"] + A3["AI Agent C"] + end + + FE <-->|"AG-UI Protocol"| A1 + FE <-->|"AG-UI Protocol"| P + P <-->|"AG-UI Protocol"| A2 + P <-->|"AG-UI Protocol"| A3 + + class P mintStyle; + classDef mintStyle fill:#E0F7E9,stroke:#66BB6A,stroke-width:2px,color:#000000; + + %% Apply slight border radius to each node + style FE rx:5, ry:5; + style A1 rx:5, ry:5; + style P rx:5, ry:5; + style A2 rx:5, ry:5; + style A3 rx:5, ry:5; +``` + +* **Front-end**: The application (chat or any AI-enabled app) that communicates + over AG-UI +* **AI Agent A**: An agent that the front-end can connect to directly without + going through the proxy +* **Secure Proxy**: An intermediary proxy that securely routes requests from the + front-end to multiple AI agents +* **Agents B and C**: Agents managed by the proxy service + +## Technical Overview + +AG-UI is designed to be lightweight and minimally opinionated, making it easy to +integrate with a wide range of agent implementations. The protocol's flexibility +comes from its simple requirements: + +1. **Event-Driven Communication**: Agents need to emit any of the 16 + standardized event types during execution, creating a stream of updates that + clients can process. + +2. **Bidirectional Interaction**: Agents accept input from users, enabling + collaborative workflows where humans and AI work together seamlessly. + +The protocol includes a built-in middleware layer that maximizes compatibility +in two key ways: + +* **Flexible Event Structure**: Events don't need to match AG-UI's format + exactly—they just need to be AG-UI-compatible. This allows existing agent + frameworks to adapt their native event formats with minimal effort. + +* **Transport Agnostic**: AG-UI doesn't mandate how events are delivered, + supporting various transport mechanisms including Server-Sent Events (SSE), + webhooks, WebSockets, and more. This flexibility lets developers choose the + transport that best fits their architecture. + +This pragmatic approach makes AG-UI easy to adopt without requiring major +changes to existing agent implementations or frontend applications. + +## Comparison with other protocols + +AG-UI focuses explicitly and specifically on the agent-user interactivity layer. +It does not compete with protocols such as A2A (Agent-to-Agent protocol) and MCP +(Model Context Protocol). + +For example, the same agent may communicate with another agent via A2A while +communicating with the user via AG-UI, and while calling tools provided by an +MCP server. + +These protocols serve complementary purposes in the agent ecosystem: + +* **AG-UI**: Handles human-in-the-loop interaction and streaming UI updates +* **A2A**: Facilitates agent-to-agent communication and collaboration +* **MCP**: Standardizes tool calls and context handling across different models + +## Quick Start + +Choose the path that fits your needs: + + + + Connect AG-UI with existing protocols, in process agents or custom solutions + **using TypeScript** + + + + Implement AG-UI compatible servers **using Python or TypeScript** + + + +## Resources + +Explore guides, tools, and integrations to help you build, optimize, and extend +your AG-UI implementation. These resources cover everything from practical +development workflows to debugging techniques. + + + + Discover ready-to-use AG-UI integrations across popular agent frameworks and + platforms + + + + Use Cursor to build AG-UI implementations faster + + + + Fix common issues when working with AG-UI servers and clients + + + +## Explore AG-UI + +Dive deeper into AG-UI's core concepts and capabilities: + + + + Understand how AG-UI connects agents, protocols, and front-ends + + + + Learn about AG-UI's communication mechanism + + + +## Contributing + +Want to contribute? Check out our +[Contributing Guide](/development/contributing) to learn how you can help +improve AG-UI. + +## Support and Feedback + +Here's how to get help or provide feedback: + +* For bug reports and feature requests related to the AG-UI specification, SDKs, + or documentation (open source), please + [create a GitHub issue](https://github.com/ag-ui-protocol) +* For discussions or Q\&A about the AG-UI specification, use the + [specification discussions](https://github.com/ag-ui-protocol/specification/discussions) +* For discussions or Q\&A about other AG-UI open source components, use the + [organization discussions](https://github.com/orgs/ag-ui-protocol/discussions) + + +# Build applications +Source: https://docs.ag-ui.com/quickstart/applications + +Build agentic applications utilizing compatible event AG-UI event streams + +# Introduction + +AG-UI provides a concise, event-driven protocol that lets any agent stream rich, +structured output to any client. It can be used to connect any agentic system to +any client. + +A client is defined as any system that can receieve, display, and respond to +AG-UI events. For more information on existing clients and integrations, see +the [integrations](/integrations) page. + +# Automatic Setup + +AG-UI provides a CLI tool to automatically create or scaffold a new application with any client and server. + +```sh +npx create-ag-ui-app@latest +``` + + + + +# Build clients +Source: https://docs.ag-ui.com/quickstart/clients + +Build a conversational CLI agent from scratch using AG-UI and Mastra + +# Introduction + +A client implementation allows you to **build conversational applications that +leverage AG-UI's event-driven protocol**. This approach creates a direct +interface between your users and AI agents, demonstrating direct access to the +AG-UI protocol. + +## When to use a client implementation + +Building your own client is useful if you want to explore/hack on the AG-UI +protocol. For production use, use a full-featured client like +[CopilotKit ](https://copilokit.ai). + +## What you'll build + +In this guide, we'll create a CLI client that: + +1. Uses the `MastraAgent` from `@ag-ui/mastra` +2. Connects to OpenAI's GPT-4o model +3. Implements a weather tool for real-world functionality +4. Provides an interactive chat interface in the terminal + +Let's get started! + +## Prerequisites + +Before we begin, make sure you have: + +* [Node.js](https://nodejs.org/) **v18 or later** +* An **OpenAI API key** +* [pnpm](https://pnpm.io/) package manager + +### 1. Provide your OpenAI API key + +First, let's set up your API key: + +```bash +# Set your OpenAI API key +export OPENAI_API_KEY=your-api-key-here +``` + +### 2. Install pnpm + +If you don't have pnpm installed: + +```bash +# Install pnpm +npm install -g pnpm +``` + +## Step 1 – Initialize your project + +Create a new directory for your AG-UI client: + +```bash +mkdir my-ag-ui-client +cd my-ag-ui-client +``` + +Initialize a new Node.js project: + +```bash +pnpm init +``` + +### Set up TypeScript and basic configuration + +Install TypeScript and essential development dependencies: + +```bash +pnpm add -D typescript @types/node tsx +``` + +Create a `tsconfig.json` file: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +Update your `package.json` scripts: + +```json +{ + // ... + + "scripts": { + "start": "tsx src/index.ts", + "dev": "tsx --watch src/index.ts", + "build": "tsc", + "clean": "rm -rf dist" + } + + // ... +} +``` + +## Step 2 – Install AG-UI and dependencies + +Install the core AG-UI packages and dependencies: + +```bash +# Core AG-UI packages +pnpm add @ag-ui/client @ag-ui/core @ag-ui/mastra + +# Mastra ecosystem packages +pnpm add @mastra/core @mastra/memory @mastra/libsql + +# AI SDK and utilities +pnpm add @ai-sdk/openai zod@^3.25 +``` + +## Step 3 – Create your agent + +Let's create a basic conversational agent. Create `src/agent.ts`: + +```typescript +import { openai } from "@ai-sdk/openai" +import { Agent } from "@mastra/core/agent" +import { MastraAgent } from "@ag-ui/mastra" +import { Memory } from "@mastra/memory" +import { LibSQLStore } from "@mastra/libsql" + +export const agent = new MastraAgent({ + agent: new Agent({ + name: "AG-UI Assistant", + instructions: ` + You are a helpful AI assistant. Be friendly, conversational, and helpful. + Answer questions to the best of your ability and engage in natural conversation. + `, + model: openai("gpt-4o"), + memory: new Memory({ + storage: new LibSQLStore({ + url: "file:./assistant.db", + }), + }), + }), + threadId: "main-conversation", +}) +``` + +### What's happening in the agent? + +1. **MastraAgent** – We wrap a Mastra Agent with the AG-UI protocol adapter +2. **Model Configuration** – We use OpenAI's GPT-4o for high-quality responses +3. **Memory Setup** – We configure persistent memory using LibSQL for + conversation context +4. **Instructions** – We give the agent basic guidelines for helpful + conversation + +## Step 4 – Create the CLI interface + +Now let's create the interactive chat interface. Create `src/index.ts`: + +```typescript +import * as readline from "readline" +import { agent } from "./agent" +import { randomUUID } from "node:crypto" + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}) + +async function chatLoop() { + console.log("🤖 AG-UI Assistant started!") + console.log("Type your messages and press Enter. Press Ctrl+D to quit.\n") + + return new Promise((resolve) => { + const promptUser = () => { + rl.question("> ", async (input) => { + if (input.trim() === "") { + promptUser() + return + } + console.log("") + + // Pause input while processing + rl.pause() + + // Add user message to conversation + agent.messages.push({ + id: randomUUID(), + role: "user", + content: input.trim(), + }) + + try { + // Run the agent with event handlers + await agent.runAgent( + {}, // No additional configuration needed + { + onTextMessageStartEvent() { + process.stdout.write("🤖 Assistant: ") + }, + onTextMessageContentEvent({ event }) { + process.stdout.write(event.delta) + }, + onTextMessageEndEvent() { + console.log("\n") + }, + } + ) + } catch (error) { + console.error("❌ Error:", error) + } + + // Resume input + rl.resume() + promptUser() + }) + } + + // Handle Ctrl+D to quit + rl.on("close", () => { + console.log("\n👋 Thanks for using AG-UI Assistant!") + resolve() + }) + + promptUser() + }) +} + +async function main() { + await chatLoop() +} + +main().catch(console.error) +``` + +### What's happening in the CLI interface? + +1. **Readline Interface** – We create an interactive prompt for user input +2. **Message Management** – We add each user input to the agent's conversation + history +3. **Event Handling** – We listen to AG-UI events to provide real-time feedback +4. **Streaming Display** – We show the agent's response as it's being generated + +## Step 5 – Test your assistant + +Let's run your new AG-UI client: + +```bash +pnpm dev +``` + +You should see: + +``` +🤖 AG-UI Assistant started! +Type your messages and press Enter. Press Ctrl+D to quit. + +> +``` + +Try asking questions like: + +* "Hello! How are you?" +* "What can you help me with?" +* "Tell me a joke" +* "Explain quantum computing in simple terms" + +You'll see the agent respond with streaming text in real-time! + +## Step 6 – Understanding the AG-UI event flow + +Let's break down what happens when you send a message: + +1. **User Input** – You type a question and press Enter +2. **Message Added** – Your input is added to the conversation history +3. **Agent Processing** – The agent analyzes your request and formulates a + response +4. **Response Generation** – The agent streams its response back +5. **Streaming Output** – You see the response appear word by word + +### Event types you're handling: + +* `onTextMessageStartEvent` – Agent starts responding +* `onTextMessageContentEvent` – Each chunk of the response +* `onTextMessageEndEvent` – Response is complete + +## Step 7 – Add tool functionality + +Now that you have a working chat interface, let's add some real-world +capabilities by creating tools. We'll start with a weather tool. + +### Create your first tool + +Let's create a weather tool that your agent can use. Create the directory +structure: + +```bash +mkdir -p src/tools +``` + +Create `src/tools/weather.tool.ts`: + +```typescript +import { createTool } from "@mastra/core/tools" +import { z } from "zod" + +interface GeocodingResponse { + results: { + latitude: number + longitude: number + name: string + }[] +} + +interface WeatherResponse { + current: { + time: string + temperature_2m: number + apparent_temperature: number + relative_humidity_2m: number + wind_speed_10m: number + wind_gusts_10m: number + weather_code: number + } +} + +export const weatherTool = createTool({ + id: "get-weather", + description: "Get current weather for a location", + inputSchema: z.object({ + location: z.string().describe("City name"), + }), + outputSchema: z.object({ + temperature: z.number(), + feelsLike: z.number(), + humidity: z.number(), + windSpeed: z.number(), + windGust: z.number(), + conditions: z.string(), + location: z.string(), + }), + execute: async ({ context }) => { + return await getWeather(context.location) + }, +}) + +const getWeather = async (location: string) => { + const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent( + location + )}&count=1` + const geocodingResponse = await fetch(geocodingUrl) + const geocodingData = (await geocodingResponse.json()) as GeocodingResponse + + if (!geocodingData.results?.[0]) { + throw new Error(`Location '${location}' not found`) + } + + const { latitude, longitude, name } = geocodingData.results[0] + + const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code` + + const response = await fetch(weatherUrl) + const data = (await response.json()) as WeatherResponse + + return { + temperature: data.current.temperature_2m, + feelsLike: data.current.apparent_temperature, + humidity: data.current.relative_humidity_2m, + windSpeed: data.current.wind_speed_10m, + windGust: data.current.wind_gusts_10m, + conditions: getWeatherCondition(data.current.weather_code), + location: name, + } +} + +function getWeatherCondition(code: number): string { + const conditions: Record = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Foggy", + 48: "Depositing rime fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 56: "Light freezing drizzle", + 57: "Dense freezing drizzle", + 61: "Slight rain", + 63: "Moderate rain", + 65: "Heavy rain", + 66: "Light freezing rain", + 67: "Heavy freezing rain", + 71: "Slight snow fall", + 73: "Moderate snow fall", + 75: "Heavy snow fall", + 77: "Snow grains", + 80: "Slight rain showers", + 81: "Moderate rain showers", + 82: "Violent rain showers", + 85: "Slight snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm with slight hail", + 99: "Thunderstorm with heavy hail", + } + return conditions[code] || "Unknown" +} +``` + +### What's happening in the weather tool? + +1. **Tool Definition** – We use `createTool` from Mastra to define the tool's + interface +2. **Input Schema** – We specify that the tool accepts a location string +3. **Output Schema** – We define the structure of the weather data returned +4. **API Integration** – We fetch data from Open-Meteo's free weather API +5. **Data Processing** – We convert weather codes to human-readable conditions + +### Update your agent + +Now let's update our agent to use the weather tool. Update `src/agent.ts`: + +```typescript +import { weatherTool } from "./tools/weather.tool" // <--- Import the tool + +export const agent = new MastraAgent({ + agent: new Agent({ + // ... + + tools: { weatherTool }, // <--- Add the tool to the agent + + // ... + }), + threadId: "main-conversation", +}) +``` + +### Update your CLI to handle tools + +Update your CLI interface in `src/index.ts` to handle tool events: + +```typescript +// Add these new event handlers to your agent.runAgent call: +await agent.runAgent( + {}, // No additional configuration needed + { + // ... existing event handlers ... + + onToolCallStartEvent({ event }) { + console.log("🔧 Tool call:", event.toolCallName) + }, + onToolCallArgsEvent({ event }) { + process.stdout.write(event.delta) + }, + onToolCallEndEvent() { + console.log("") + }, + onToolCallResultEvent({ event }) { + if (event.content) { + console.log("🔍 Tool call result:", event.content) + } + }, + } +) +``` + +### Test your weather tool + +Now restart your application and try asking about weather: + +```bash +pnpm dev +``` + +Try questions like: + +* "What's the weather like in London?" +* "How's the weather in Tokyo today?" +* "Is it raining in Seattle?" + +You'll see the agent use the weather tool to fetch real data and provide +detailed responses! + +## Step 8 – Add more functionality + +### Create a browser tool + +Let's add a web browsing capability. First install the `open` package: + +```bash +pnpm add open +``` + +Create `src/tools/browser.tool.ts`: + +```typescript +import { createTool } from "@mastra/core/tools" +import { z } from "zod" +import { open } from "open" + +export const browserTool = createTool({ + id: "open-browser", + description: "Open a URL in the default web browser", + inputSchema: z.object({ + url: z.string().url().describe("The URL to open"), + }), + outputSchema: z.object({ + success: z.boolean(), + message: z.string(), + }), + execute: async ({ context }) => { + try { + await open(context.url) + return { + success: true, + message: `Opened ${context.url} in your default browser`, + } + } catch (error) { + return { + success: false, + message: `Failed to open browser: ${error}`, + } + } + }, +}) +``` + +### Update your agent with both tools + +Update `src/agent.ts` to include both tools: + +```typescript +import { openai } from "@ai-sdk/openai" +import { Agent } from "@mastra/core/agent" +import { MastraAgent } from "@ag-ui/mastra" +import { Memory } from "@mastra/memory" +import { LibSQLStore } from "@mastra/libsql" +import { weatherTool } from "./tools/weather.tool" +import { browserTool } from "./tools/browser.tool" + +export const agent = new MastraAgent({ + agent: new Agent({ + name: "AG-UI Assistant", + instructions: ` + You are a helpful assistant with weather and web browsing capabilities. + + For weather queries: + - Always ask for a location if none is provided + - Use the weatherTool to fetch current weather data + + For web browsing: + - Always use full URLs (e.g., "https://www.google.com") + - Use the browserTool to open web pages + + Be friendly and helpful in all interactions! + `, + model: openai("gpt-4o"), + tools: { weatherTool, browserTool }, // Add both tools + memory: new Memory({ + storage: new LibSQLStore({ + url: "file:./assistant.db", + }), + }), + }), + threadId: "main-conversation", +}) +``` + +Now you can ask your assistant to open websites: "Open Google for me" or "Show +me the weather website". + +## Step 9 – Deploy your client + +### Building your client + +Create a production build: + +```bash +pnpm build +``` + +### Create a startup script + +Add to your `package.json`: + +```json +{ + "bin": { + "weather-assistant": "./dist/index.js" + } +} +``` + +Add a shebang to your built `dist/index.js`: + +```javascript +#!/usr/bin/env node +// ... rest of your compiled code +``` + +Make it executable: + +```bash +chmod +x dist/index.js +``` + +### Link globally + +Install your CLI globally: + +```bash +pnpm link --global +``` + +Now you can run `weather-assistant` from anywhere! + +## Extending your client + +Your AG-UI client is now a solid foundation. Here are some ideas for +enhancement: + +### Add more tools + +* **Calculator tool** – For mathematical operations +* **File system tool** – For reading/writing files +* **API tools** – For connecting to other services +* **Database tools** – For querying data + +### Improve the interface + +* **Rich formatting** – Use libraries like `chalk` for colored output +* **Progress indicators** – Show loading states for long operations +* **Configuration files** – Allow users to customize settings +* **Command-line arguments** – Support different modes and options + +### Add persistence + +* **Conversation history** – Save and restore chat sessions +* **User preferences** – Remember user settings +* **Tool results caching** – Cache expensive API calls + +## Share your client + +Built something useful? Consider sharing it with the community: + +1. **Open source it** – Publish your code on GitHub +2. **Publish to npm** – Make it installable via `npm install` +3. **Create documentation** – Help others understand and extend your work +4. **Join discussions** – Share your experience in the + [AG-UI GitHub Discussions](https://github.com/orgs/ag-ui-protocol/discussions) + +## Conclusion + +You've built a complete AG-UI client from scratch! Your weather assistant +demonstrates the core concepts: + +* **Event-driven architecture** with real-time streaming +* **Tool integration** for real-world functionality +* **Conversation memory** for context retention +* **Interactive CLI interface** for user engagement + +From here, you can extend your client to support any use case – from simple CLI +tools to complex conversational applications. The AG-UI protocol provides the +foundation, and your creativity provides the possibilities. + +Happy building! 🚀 + + +# Introduction +Source: https://docs.ag-ui.com/quickstart/introduction + +Learn how to get started building an AG-UI integration + +