From 25d04907e2d41839c2baf4fa568202e2ffddb1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Thu, 26 Sep 2024 17:06:16 +0800 Subject: [PATCH] feat: useXAgent (#125) * chore: bubble variant * chore: customize * chore: lock typing if first render * chore: tmp record * chore: seq list * docs: add api doc * test: coverage * test: update snapshot * chore: defaultMessages no need fill all * refactor: rename * chore: tmp of ts def * chore: base fin * chore: demo of it * chore: arr return * docs: demo of streaming * docs: api * test: of useXAgent * test: coverage * chore: fix demo --- .dumi/theme/themeConfig.ts | 4 +- components/bubble/Bubble.tsx | 21 +- components/bubble/BubbleList.tsx | 28 +- .../__snapshots__/demo-extend.test.ts.snap | 512 ++++++++++++++++-- .../__tests__/__snapshots__/demo.test.ts.snap | 444 ++++++++++++++- .../__snapshots__/index.test.tsx.snap | 4 +- components/bubble/demo/list-custom.md | 7 + components/bubble/demo/list-custom.tsx | 66 +++ components/bubble/demo/variant.md | 7 + components/bubble/demo/variant.tsx | 31 ++ components/bubble/hooks/useDisplayData.ts | 42 ++ components/bubble/hooks/useListData.ts | 2 + components/bubble/hooks/useTypedEffect.ts | 26 +- components/bubble/index.en-US.md | 10 +- components/bubble/index.zh-CN.md | 10 +- components/bubble/interface.ts | 6 +- components/bubble/style/index.ts | 16 +- components/index.ts | 2 + components/prompts/style/index.ts | 3 + .../__tests__/__snapshots__/demo.test.ts.snap | 71 +++ components/useXAgent/__tests__/demo.test.ts | 3 + components/useXAgent/__tests__/image.test.ts | 5 + components/useXAgent/__tests__/index.test.tsx | 105 ++++ components/useXAgent/demo/custom.md | 7 + components/useXAgent/demo/custom.tsx | 72 +++ components/useXAgent/demo/preset.md | 7 + components/useXAgent/demo/preset.tsx | 14 + components/useXAgent/index.en-US.md | 77 +++ components/useXAgent/index.ts | 114 ++++ components/useXAgent/index.zh-CN.md | 78 +++ components/useXAgent/presets/gpt-3.5-turbo.ts | 7 + .../__tests__/__snapshots__/demo.test.ts.snap | 306 +++++++++++ components/useXChat/__tests__/demo.test.ts | 3 + components/useXChat/__tests__/image.test.ts | 5 + components/useXChat/__tests__/index.test.tsx | 220 ++++++++ components/useXChat/demo/basic.md | 7 + components/useXChat/demo/basic.tsx | 75 +++ components/useXChat/demo/stream.md | 7 + components/useXChat/demo/stream.tsx | 67 +++ components/useXChat/demo/suggestions.md | 7 + components/useXChat/demo/suggestions.tsx | 145 +++++ components/useXChat/index.en-US.md | 50 ++ components/useXChat/index.ts | 238 ++++++++ components/useXChat/index.zh-CN.md | 49 ++ components/useXChat/useSyncState.ts | 12 + tests/__snapshots__/index.test.ts.snap | 2 + tests/shared/demoTest.tsx | 27 +- 47 files changed, 2920 insertions(+), 101 deletions(-) create mode 100644 components/bubble/demo/list-custom.md create mode 100644 components/bubble/demo/list-custom.tsx create mode 100644 components/bubble/demo/variant.md create mode 100644 components/bubble/demo/variant.tsx create mode 100644 components/bubble/hooks/useDisplayData.ts create mode 100644 components/useXAgent/__tests__/__snapshots__/demo.test.ts.snap create mode 100644 components/useXAgent/__tests__/demo.test.ts create mode 100644 components/useXAgent/__tests__/image.test.ts create mode 100644 components/useXAgent/__tests__/index.test.tsx create mode 100644 components/useXAgent/demo/custom.md create mode 100644 components/useXAgent/demo/custom.tsx create mode 100644 components/useXAgent/demo/preset.md create mode 100644 components/useXAgent/demo/preset.tsx create mode 100644 components/useXAgent/index.en-US.md create mode 100644 components/useXAgent/index.ts create mode 100644 components/useXAgent/index.zh-CN.md create mode 100644 components/useXAgent/presets/gpt-3.5-turbo.ts create mode 100644 components/useXChat/__tests__/__snapshots__/demo.test.ts.snap create mode 100644 components/useXChat/__tests__/demo.test.ts create mode 100644 components/useXChat/__tests__/image.test.ts create mode 100644 components/useXChat/__tests__/index.test.tsx create mode 100644 components/useXChat/demo/basic.md create mode 100644 components/useXChat/demo/basic.tsx create mode 100644 components/useXChat/demo/stream.md create mode 100644 components/useXChat/demo/stream.tsx create mode 100644 components/useXChat/demo/suggestions.md create mode 100644 components/useXChat/demo/suggestions.tsx create mode 100644 components/useXChat/index.en-US.md create mode 100644 components/useXChat/index.ts create mode 100644 components/useXChat/index.zh-CN.md create mode 100644 components/useXChat/useSyncState.ts diff --git a/.dumi/theme/themeConfig.ts b/.dumi/theme/themeConfig.ts index 60ac9806..8ca36a63 100644 --- a/.dumi/theme/themeConfig.ts +++ b/.dumi/theme/themeConfig.ts @@ -20,7 +20,8 @@ export default { 'Data Display': 4, Feedback: 5, Other: 6, - Deprecated: 7, + Runtime: 7, + Deprecated: 8, 组件总览: -1, 通用: 0, @@ -30,6 +31,7 @@ export default { 数据展示: 4, 反馈: 5, 其他: 6, + 运行时: 8, 废弃: 7, // Design diff --git a/components/bubble/Bubble.tsx b/components/bubble/Bubble.tsx index 9c0229af..5b2a945c 100644 --- a/components/bubble/Bubble.tsx +++ b/components/bubble/Bubble.tsx @@ -34,6 +34,8 @@ const Bubble: React.ForwardRefRenderFunction = (props, r typing, content = '', messageRender, + variant = 'filled', + onTypingComplete, ...otherHtmlProps } = props; @@ -68,6 +70,20 @@ const Bubble: React.ForwardRefRenderFunction = (props, r onUpdate?.(); }, [typedContent]); + const triggerTypingCompleteRef = React.useRef(false); + React.useEffect(() => { + if (!isTyping && !loading) { + // StrictMode will trigger this twice, + // So we need a flag to avoid that + if (!triggerTypingCompleteRef.current) { + triggerTypingCompleteRef.current = true; + onTypingComplete?.(); + } + } else { + triggerTypingCompleteRef.current = false; + } + }, [isTyping, loading]); + // ============================ Styles ============================ const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); @@ -89,7 +105,7 @@ const Bubble: React.ForwardRefRenderFunction = (props, r const avatarNode = React.isValidElement(avatar) ? avatar : ; // =========================== Content ============================ - const mergedContent = messageRender ? messageRender(typedContent) : typedContent; + const mergedContent = messageRender ? messageRender(typedContent as any) : typedContent; // ============================ Render ============================ return wrapCSSVar( @@ -127,11 +143,12 @@ const Bubble: React.ForwardRefRenderFunction = (props, r }} className={classnames( `${prefixCls}-content`, + `${prefixCls}-content-${variant}`, contextConfig.classNames.content, classNames.content, )} > - {loading ? : mergedContent} + {loading ? : (mergedContent as React.ReactNode)} , ); diff --git a/components/bubble/BubbleList.tsx b/components/bubble/BubbleList.tsx index 86c3c34b..14d23e54 100644 --- a/components/bubble/BubbleList.tsx +++ b/components/bubble/BubbleList.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { useXProviderContext } from '../x-provider'; import Bubble, { BubbleContext } from './Bubble'; import type { BubbleRef } from './Bubble'; +import useDisplayData from './hooks/useDisplayData'; import useListData from './hooks/useListData'; import type { BubbleProps } from './interface'; import useStyle from './style'; @@ -64,9 +65,21 @@ const BubbleList: React.ForwardRefRenderFunction const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); + // ============================ Typing ============================ + const [initialized, setInitialized] = React.useState(false); + + React.useEffect(() => { + setInitialized(true); + return () => { + setInitialized(false); + }; + }, []); + // ============================= Data ============================= const mergedData = useListData(items, roles); + const [displayData, onTypingComplete] = useDisplayData(mergedData); + // ============================ Scroll ============================ // Is current scrollTop at the end. User scroll will make this false. const [scrollReachEnd, setScrollReachEnd] = React.useState(true); @@ -91,7 +104,7 @@ const BubbleList: React.ForwardRefRenderFunction React.useEffect(() => { if (autoScroll) { // New date come, the origin last one is the second last one - const lastItemKey = mergedData[mergedData.length - 2]?.key; + const lastItemKey = displayData[displayData.length - 2]?.key; const bubbleInst = bubbleRefs.current[lastItemKey!]; // Auto scroll if last 2 item is visible @@ -107,7 +120,7 @@ const BubbleList: React.ForwardRefRenderFunction } } } - }, [mergedData.length]); + }, [displayData.length]); // ========================== Outer Ref =========================== React.useImperativeHandle(ref, () => ({ @@ -125,8 +138,8 @@ const BubbleList: React.ForwardRefRenderFunction if (bubbleInst) { // Block current auto scrolling - const index = mergedData.findIndex((dataItem) => dataItem.key === key); - setScrollReachEnd(index === mergedData.length - 1); + const index = displayData.findIndex((dataItem) => dataItem.key === key); + setScrollReachEnd(index === displayData.length - 1); // Do native scroll bubbleInst.nativeElement.scrollIntoView({ @@ -164,7 +177,7 @@ const BubbleList: React.ForwardRefRenderFunction ref={listRef} onScroll={onInternalScroll} > - {mergedData.map(({ key, ...bubble }) => ( + {displayData.map(({ key, ...bubble }) => ( delete bubbleRefs.current[key]; } }} + typing={initialized ? bubble.typing : false} + onTypingComplete={() => { + bubble.onTypingComplete?.(); + onTypingComplete(key); + }} /> ))} diff --git a/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap index 1e7b659f..9f7380e5 100644 --- a/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -2,16 +2,16 @@ exports[`renders components/bubble/demo/avatar-and-placement.tsx extend context correctly 1`] = `
Good morning, how are you?
What a beautiful day!
Hi, good morning, I'm fine!
Thank you!
@@ -129,10 +129,10 @@ exports[`renders components/bubble/demo/avatar-and-placement.tsx extend context exports[`renders components/bubble/demo/basic.tsx extend context correctly 1`] = `
hello world !
@@ -143,14 +143,14 @@ exports[`renders components/bubble/demo/basic.tsx extend context correctly 2`] = exports[`renders components/bubble/demo/list.tsx extend context correctly 1`] = `
Mock user content.
@@ -217,7 +217,7 @@ exports[`renders components/bubble/demo/list.tsx extend context correctly 1`] = class="ant-bubble-avatar" >
- M + Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content.
Mock user content.
@@ -291,18 +291,272 @@ exports[`renders components/bubble/demo/list.tsx extend context correctly 1`] = exports[`renders components/bubble/demo/list.tsx extend context correctly 2`] = `[]`; +exports[`renders components/bubble/demo/list-custom.tsx extend context correctly 1`] = ` +
+
+
+ + + + + +
+
+ Normal message +
+
+
+
+ + + + + +
+
+ + ReactNode message + +
+
+
+
+ +
+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+
+`; + +exports[`renders components/bubble/demo/list-custom.tsx extend context correctly 2`] = `[]`; + exports[`renders components/bubble/demo/loading.tsx extend context correctly 1`] = `
Loading state: + +
  • + +
  • +
  • + +
  • + +
    +
    +
    +`; + +exports[`renders components/bubble/demo/variant.tsx extend context correctly 2`] = `[]`; diff --git a/components/bubble/__tests__/__snapshots__/demo.test.ts.snap b/components/bubble/__tests__/__snapshots__/demo.test.ts.snap index b7ac0325..a39eb56a 100644 --- a/components/bubble/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/bubble/__tests__/__snapshots__/demo.test.ts.snap @@ -36,7 +36,7 @@ exports[`renders components/bubble/demo/avatar-and-placement.tsx correctly 1`] =
    Good morning, how are you?
    @@ -58,7 +58,7 @@ exports[`renders components/bubble/demo/avatar-and-placement.tsx correctly 1`] =
    What a beautiful day!
    @@ -95,7 +95,7 @@ exports[`renders components/bubble/demo/avatar-and-placement.tsx correctly 1`] =
    Hi, good morning, I'm fine!
    @@ -117,7 +117,7 @@ exports[`renders components/bubble/demo/avatar-and-placement.tsx correctly 1`] =
    Thank you!
    @@ -130,7 +130,7 @@ exports[`renders components/bubble/demo/basic.tsx correctly 1`] = ` class="ant-bubble css-var-R0 ant-bubble-start" >
    hello world !
    @@ -199,13 +199,13 @@ exports[`renders components/bubble/demo/list.tsx correctly 1`] = `
    Mock user content.
    @@ -238,9 +238,9 @@ exports[`renders components/bubble/demo/list.tsx correctly 1`] = `
    - M + Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content. Mock AI content.
    Mock user content.
    @@ -285,6 +285,258 @@ exports[`renders components/bubble/demo/list.tsx correctly 1`] = ` `; +exports[`renders components/bubble/demo/list-custom.tsx correctly 1`] = ` +
    +
    +
    + + + + + +
    +
    + Normal message +
    +
    +
    +
    + + + + + +
    +
    + + ReactNode message + +
    +
    +
    +
    + +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
    +`; + exports[`renders components/bubble/demo/loading.tsx correctly 1`] = `
    A
    @@ -465,3 +717,169 @@ exports[`renders components/bubble/demo/typing.tsx correctly 1`] = ` `; + +exports[`renders components/bubble/demo/variant.tsx correctly 1`] = ` +
    +
    + + + + + +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +`; diff --git a/components/bubble/__tests__/__snapshots__/index.test.tsx.snap b/components/bubble/__tests__/__snapshots__/index.test.tsx.snap index 16cae205..43635285 100644 --- a/components/bubble/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/bubble/__tests__/__snapshots__/index.test.tsx.snap @@ -5,7 +5,7 @@ exports[`bubble Bubble component work 1`] = ` class="ant-bubble ant-bubble-start" >
    test
    @@ -17,7 +17,7 @@ exports[`bubble rtl render component should be rendered correctly in RTL directi class="ant-bubble css-var-r1 ant-bubble-start ant-bubble-rtl" >
    test
    diff --git a/components/bubble/demo/list-custom.md b/components/bubble/demo/list-custom.md new file mode 100644 index 00000000..cf59d93e --- /dev/null +++ b/components/bubble/demo/list-custom.md @@ -0,0 +1,7 @@ +## zh-CN + +自定义气泡列表内容,这对于个性化定制场景非常有用。 + +## en-US + +Customize the content of the bubble list, which is very useful for personalized customization scenarios. diff --git a/components/bubble/demo/list-custom.tsx b/components/bubble/demo/list-custom.tsx new file mode 100644 index 00000000..36b2870d --- /dev/null +++ b/components/bubble/demo/list-custom.tsx @@ -0,0 +1,66 @@ +import { CoffeeOutlined, FireOutlined, SmileOutlined, UserOutlined } from '@ant-design/icons'; +import { Bubble, Prompts } from '@ant-design/x'; +import { GetProp, Typography } from 'antd'; +import React from 'react'; + +const roles: GetProp = { + ai: { + placement: 'start', + typing: true, + avatar: { icon: , style: { background: '#fde3cf' } }, + }, + suggestion: { + placement: 'start', + avatar: { icon: , style: { visibility: 'hidden' } }, + variant: 'borderless', + messageRender: (items) => , + }, +}; + +const App = () => { + return ( + ReactNode message, + }, + + // Other components + { + key: 2, + role: 'suggestion', + content: [ + { + key: '6', + icon: , + description: 'How to rest effectively after long hours of work?', + }, + { + key: '7', + icon: , + description: 'What are the secrets to maintaining a positive mindset?', + }, + { + key: '8', + icon: , + description: 'How to stay calm under immense pressure?', + }, + ], + }, + ]} + /> + ); +}; + +export default App; diff --git a/components/bubble/demo/variant.md b/components/bubble/demo/variant.md new file mode 100644 index 00000000..400139aa --- /dev/null +++ b/components/bubble/demo/variant.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `variant` 属性设置气泡的样式变体。 + +## en-US + +Set the style variant of the bubble through the `variant` property. diff --git a/components/bubble/demo/variant.tsx b/components/bubble/demo/variant.tsx new file mode 100644 index 00000000..df536190 --- /dev/null +++ b/components/bubble/demo/variant.tsx @@ -0,0 +1,31 @@ +import { CoffeeOutlined, FireOutlined, SmileOutlined, UserOutlined } from '@ant-design/icons'; +import { Bubble, Prompts, PromptsProps } from '@ant-design/x'; +import React from 'react'; + +const items: PromptsProps['items'] = [ + { + key: '6', + icon: , + description: 'How to rest effectively after long hours of work?', + }, + { + key: '7', + icon: , + description: 'What are the secrets to maintaining a positive mindset?', + }, + { + key: '8', + icon: , + description: 'How to stay calm under immense pressure?', + }, +]; + +const App = () => ( + }} + content={} + /> +); + +export default App; diff --git a/components/bubble/hooks/useDisplayData.ts b/components/bubble/hooks/useDisplayData.ts new file mode 100644 index 00000000..b7dbbb72 --- /dev/null +++ b/components/bubble/hooks/useDisplayData.ts @@ -0,0 +1,42 @@ +import { useEvent } from 'rc-util'; +import React from 'react'; +import { ListItemType } from './useListData'; + +export default function useDisplayData(items: ListItemType[]) { + const [displayCount, setDisplayCount] = React.useState(items.length); + + const displayList = React.useMemo(() => items.slice(0, displayCount), [items, displayCount]); + + const displayListLastKey = React.useMemo(() => { + const lastItem = displayList[displayList.length - 1]; + return lastItem ? lastItem.key : null; + }, [displayList]); + + // When `items` changed, we replaced with latest one + React.useEffect(() => { + if (displayList.length && displayList.every((item, index) => item.key === items[index]?.key)) { + return; + } + + if (displayList.length === 0) { + setDisplayCount(1); + } else { + // Find diff index + for (let i = 0; i < displayList.length; i += 1) { + if (displayList[i].key !== items[i]?.key) { + setDisplayCount(i); + break; + } + } + } + }, [items]); + + // Continue to show if last one finished typing + const onTypingComplete = useEvent((key: string | number) => { + if (key === displayListLastKey) { + setDisplayCount(displayCount + 1); + } + }); + + return [displayList, onTypingComplete] as const; +} diff --git a/components/bubble/hooks/useListData.ts b/components/bubble/hooks/useListData.ts index c147b16b..0f5e46cb 100644 --- a/components/bubble/hooks/useListData.ts +++ b/components/bubble/hooks/useListData.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import type { BubbleDataType, BubbleListProps } from '../BubbleList'; import type { BubbleProps } from '../interface'; +export type ListItemType = ReturnType[number]; + export default function useListData( items: BubbleListProps['items'], roles?: BubbleListProps['roles'], diff --git a/components/bubble/hooks/useTypedEffect.ts b/components/bubble/hooks/useTypedEffect.ts index 4d85951d..0d16b5ce 100644 --- a/components/bubble/hooks/useTypedEffect.ts +++ b/components/bubble/hooks/useTypedEffect.ts @@ -1,30 +1,38 @@ -import * as React from 'react'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; +import * as React from 'react'; + +function isString(str: any): str is string { + return typeof str === 'string'; +} /** * Return typed content and typing status when typing is enabled. * Or return content directly. */ const useTypedEffect = ( - content: string, + content: React.ReactNode | object, typingEnabled: boolean, typingStep: number, typingInterval: number, -): [typedContent: string, isTyping: boolean] => { - const [prevContent, setPrevContent] = React.useState(''); +): [typedContent: React.ReactNode | object, isTyping: boolean] => { + const [prevContent, setPrevContent] = React.useState(''); const [typingIndex, setTypingIndex] = React.useState(1); + const mergedTypingEnabled = typingEnabled && isString(content); + // Reset typing index when content changed useLayoutEffect(() => { setPrevContent(content); - if (content.indexOf(prevContent) !== 0) { + if (!mergedTypingEnabled && isString(content)) { + setTypingIndex(content.length); + } else if (isString(content) && isString(prevContent) && content.indexOf(prevContent) !== 0) { setTypingIndex(1); } - }, [content, typingEnabled]); + }, [content]); // Start typing React.useEffect(() => { - if (typingEnabled && typingIndex < content.length) { + if (mergedTypingEnabled && typingIndex < content.length) { const id = setTimeout(() => { setTypingIndex((prev) => prev + typingStep); }, typingInterval); @@ -35,9 +43,9 @@ const useTypedEffect = ( } }, [typingIndex, typingEnabled, content]); - const mergedTypingContent = typingEnabled ? content.slice(0, typingIndex) : content; + const mergedTypingContent = mergedTypingEnabled ? content.slice(0, typingIndex) : content; - return [mergedTypingContent, typingEnabled && typingIndex < content.length]; + return [mergedTypingContent, mergedTypingEnabled && typingIndex < content.length]; }; export default useTypedEffect; diff --git a/components/bubble/index.en-US.md b/components/bubble/index.en-US.md index 718635c7..f941fee2 100644 --- a/components/bubble/index.en-US.md +++ b/components/bubble/index.en-US.md @@ -21,7 +21,9 @@ Often used when chatting. Loading Typing effect Content render +Variant Bubble List +Custom List Content ## API @@ -33,12 +35,14 @@ Common props ref:[Common props](/docs/react/common-props) | --- | --- | --- | --- | --- | | avatar | Avatar component | React.ReactNode | - | | | classNames | Semantic DOM class | [Record](#semantic-dom) | - | | -| styles | Semantic DOM style | [Record](#semantic-dom) | - | | -| placement | Direction of Message | `start` \| `end` | `start` | | +| content | Content of bubble | string | - | | | loading | Loading state of Message | boolean | - | | +| placement | Direction of Message | `start` \| `end` | `start` | | +| styles | Semantic DOM style | [Record](#semantic-dom) | - | | | typing | Show message with typing motion | boolean \| { step?: number, interval?: number } | false | | -| content | Content of bubble | string | - | | +| variant | Style variant | `filled` \| `borderless` | `filled` | | | messageRender | Display customized content | (content?: string) => ReactNode | - | | +| onTypingComplete | Callback when typing effect is completed. If typing is not set, it will be triggered immediately when rendering. | () => void | - | | ### Bubble.List diff --git a/components/bubble/index.zh-CN.md b/components/bubble/index.zh-CN.md index b4c4f296..5dd6057f 100644 --- a/components/bubble/index.zh-CN.md +++ b/components/bubble/index.zh-CN.md @@ -22,7 +22,9 @@ demo: 加载中 打字效果 自定义渲染 +变体 气泡列表 +自定义列表内容 ## API @@ -34,12 +36,14 @@ demo: | --- | --- | --- | --- | --- | | avatar | 展示头像 | React.ReactNode | - | | | classNames | 语义化结构 class | [Record](#semantic-dom) | - | | -| styles | 语义化结构 style | [Record](#semantic-dom) | - | | -| placement | 信息位置 | `start` \| `end` | `start` | | +| content | 聊天内容 | string | - | | | loading | 聊天内容加载状态 | boolean | - | | +| placement | 信息位置 | `start` \| `end` | `start` | | +| styles | 语义化结构 style | [Record](#semantic-dom) | - | | | typing | 设置聊天内容打字动画 | boolean \| { step?: number, interval?: number } | false | | -| content | 聊天内容 | string | - | | +| variant | 气泡样式变体 | `filled` \| `borderless` | `filled` | | | messageRender | 自定义渲染内容 | (content?: string) => ReactNode | - | | +| onTypingComplete | 打字效果完成时的回调,如果没有设置 typing 将在渲染时立刻触发 | () => void | - | | ### Bubble.List diff --git a/components/bubble/interface.ts b/components/bubble/interface.ts index 93471b91..e9fa488a 100644 --- a/components/bubble/interface.ts +++ b/components/bubble/interface.ts @@ -13,7 +13,7 @@ export interface TypingOption { type SemanticType = 'avatar' | 'content'; -export interface BubbleProps extends React.HTMLAttributes { +export interface BubbleProps extends Omit, 'content'> { prefixCls?: string; rootClassName?: string; styles?: Partial>; @@ -22,6 +22,8 @@ export interface BubbleProps extends React.HTMLAttributes { placement?: 'start' | 'end'; loading?: boolean; typing?: boolean | TypingOption; - content: string; + content?: React.ReactNode | object; messageRender?: (content: string) => React.ReactNode; + variant?: 'filled' | 'borderless'; + onTypingComplete?: VoidFunction; } diff --git a/components/bubble/style/index.ts b/components/bubble/style/index.ts index 08c52d90..53cc0bdb 100644 --- a/components/bubble/style/index.ts +++ b/components/bubble/style/index.ts @@ -75,15 +75,23 @@ const genBubbleStyle: GenerateStyle = (token) => { [`& ${componentCls}-content`]: { position: 'relative', boxSizing: 'border-box', - padding: `${unit(paddingSM)} ${unit(padding)}`, + color: colorText, fontSize: token.fontSize, lineHeight: token.lineHeight, minHeight: calc(paddingSM).mul(2).add(calc(lineHeight).mul(fontSize)).equal(), maxWidth: token.bubbleContentMaxWidth, - backgroundColor: token.colorInfoBg, - borderRadius: token.borderRadiusLG, - boxShadow: token.boxShadowTertiary, + + wordBreak: 'break-word', + + // Variant + '&-filled': { + padding: `${unit(paddingSM)} ${unit(padding)}`, + backgroundColor: token.colorInfoBg, + borderRadius: token.borderRadiusLG, + boxShadow: token.boxShadowTertiary, + }, + [`& ${componentCls}-dot`]: { position: 'relative', height: '100%', diff --git a/components/index.ts b/components/index.ts index 9dcf61bd..45ddeda6 100644 --- a/components/index.ts +++ b/components/index.ts @@ -11,4 +11,6 @@ export { default as Suggestion } from './suggestion'; export type { SuggestionProps } from './suggestion'; export { default as XProvider } from './x-provider'; export type { XProviderProps } from './x-provider'; +export { default as useXChat } from './useXChat'; +export { default as useXAgent } from './useXAgent'; export { default as version } from './version'; diff --git a/components/prompts/style/index.ts b/components/prompts/style/index.ts index 6f3bde05..98c6a13a 100644 --- a/components/prompts/style/index.ts +++ b/components/prompts/style/index.ts @@ -26,6 +26,9 @@ const genPromptsStyle: GenerateStyle = (token) => { '&::-webkit-scrollbar': { display: 'none', }, + listStyle: 'none', + paddingInlineStart: 0, + marginBlock: 0, '&-wrap': { flexWrap: 'wrap', diff --git a/components/useXAgent/__tests__/__snapshots__/demo.test.ts.snap b/components/useXAgent/__tests__/__snapshots__/demo.test.ts.snap new file mode 100644 index 00000000..fb4b4568 --- /dev/null +++ b/components/useXAgent/__tests__/__snapshots__/demo.test.ts.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/useXAgent/demo/custom.tsx correctly 1`] = ` +Array [ +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    , +