diff --git a/src/chat/content/index.scss b/src/chat/content/index.scss index d5a863073..43b625dfe 100644 --- a/src/chat/content/index.scss +++ b/src/chat/content/index.scss @@ -3,13 +3,10 @@ position: relative; width: 100%; height: 100%; - overflow: hidden; + overflow: auto; &--disabled { pointer-events: none; } - &--valid { - overflow: auto; - } } &__inner__holder { display: flex; diff --git a/src/chat/content/index.tsx b/src/chat/content/index.tsx index 26ff7833e..685b5a1c3 100644 --- a/src/chat/content/index.tsx +++ b/src/chat/content/index.tsx @@ -2,7 +2,6 @@ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useSta import classNames from 'classnames'; import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity'; -import { RobotIcon } from '../icon'; import Message from '../message'; import Prompt from '../prompt'; import { useContext } from '../useContext'; @@ -11,7 +10,6 @@ import './index.scss'; export interface IContentProps { data: PromptEntity[]; placeholder?: React.ReactNode; - robotIcon?: boolean; scrollable?: boolean; onRegenerate?: (data: MessageEntity, prompt: PromptEntity) => void; onStop?: (data: MessageEntity, prompt: PromptEntity) => void; @@ -23,7 +21,7 @@ export interface IContentRef { } const Content = forwardRef(function ( - { data, placeholder, robotIcon = true, scrollable = true, onRegenerate, onStop }, + { data, placeholder, scrollable = true, onRegenerate, onStop }, forwardedRef ) { const { maxRegenerateCount, copy, regenerate } = useContext(); @@ -100,8 +98,7 @@ const Content = forwardRef(function (
@@ -143,14 +140,7 @@ const Content = forwardRef(function ( })}
) : ( - - {placeholder} - {robotIcon && ( - - )} - + {placeholder} )} ); diff --git a/src/chat/demos/basic.tsx b/src/chat/demos/basic.tsx index 6600ad974..47d84cc1c 100644 --- a/src/chat/demos/basic.tsx +++ b/src/chat/demos/basic.tsx @@ -1,15 +1,18 @@ import React, { useEffect, useState } from 'react'; +import { LikeOutlined } from '@ant-design/icons'; import { Button } from 'antd'; -import { Chat } from 'dt-react-component'; +import { Chat, Flex } from 'dt-react-component'; import { mockSSE } from './mockSSE'; export default function () { const chat = Chat.useChat(); - const [value, setValue] = useState(''); + const [value, setValue] = useState(''); - const handleSubmit = (raw: string = value) => { - const val = raw.trim(); + const [convert, setConvert] = useState(false); + + const handleSubmit = (raw = value) => { + const val = raw?.trim(); if (chat.loading() || !val) return; setValue(''); const promptId = new Date().valueOf().toString(); @@ -35,35 +38,56 @@ export default function () { }, []); return ( -
- +
+ } + components={{ + a: ({ children }) => ( + + ), + }} + > - 有什么可以帮忙的? +
- + + handleSubmit('请告诉我一首诗')}> + 返回一首诗 + + handleSubmit('生成 CodeBlock')}> + 生成 CodeBlock + + } /> -
- handleSubmit()} - placeholder="请输入想咨询的内容…" - /> - handleSubmit()} - disabled={chat.loading() || !value} - > - - -
+ handleSubmit()} + onPressShiftEnter={() => setValue((v) => v + '\n')} + button={{ + disabled: chat.loading() || !value?.trim(), + }} + placeholder="请输入想咨询的内容…" + />
); diff --git a/src/chat/demos/button.tsx b/src/chat/demos/button.tsx index d6939088e..946708f88 100644 --- a/src/chat/demos/button.tsx +++ b/src/chat/demos/button.tsx @@ -16,33 +16,33 @@ export default function () { } + icon={} /> } + icon={} > - 发送 + 按钮 } + icon={} > - 发送 + 按钮 } + icon={} > - 发送 + 按钮 } + icon={} > AI log parsing @@ -54,37 +54,37 @@ export default function () { size="small" type="primary" disabled={disabled} - icon={} + icon={} /> } + icon={} > - 发送 + 按钮 } + icon={} > - 发送 + 按钮 } + icon={} > - 发送 + 按钮 } + icon={} > AI log parsing diff --git a/src/chat/demos/global-state/index.tsx b/src/chat/demos/global-state/index.tsx index a5a26d4e5..bd2854f09 100644 --- a/src/chat/demos/global-state/index.tsx +++ b/src/chat/demos/global-state/index.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { Button, Tabs } from 'antd'; +import { Tabs } from 'antd'; import { Chat } from 'dt-react-component'; import { Conversation, Message, MessageStatus, Prompt } from 'dt-react-component/chat/entity'; import { produce } from 'immer'; @@ -44,7 +44,7 @@ export default function () { return conversations[activeKey]; }, [activeKey, conversations]); - const handleSubmit = (val: string) => { + const handleSubmit = (val = '') => { if (!data) { const promptId = new Date().valueOf().toString(); const messageId = (new Date().valueOf() + 1).toString(); @@ -122,38 +122,38 @@ export default function () { ); } -function AI({ data, onSubmit }: { data?: Conversation; onSubmit?: (str: string) => void }) { +function AI({ data, onSubmit }: { data?: Conversation; onSubmit?: (str?: string) => void }) { const chat = Chat.useChat(); - const [value, setValue] = useState(''); + const [value, setValue] = useState(''); return ( -
+
- 有什么可以帮忙的? +
- + onSubmit?.('请告诉我一首诗')}> + 返回一首诗 + } /> -
- onSubmit?.(value)} - placeholder="请输入想咨询的内容…" - /> - onSubmit?.(value)} - disabled={chat.loading() || !value} - > - - -
+ onSubmit?.(value)} + onPressShiftEnter={() => setValue((v) => v + '\n')} + button={{ + disabled: chat.loading() || !value?.trim(), + }} + placeholder="请输入想咨询的内容…" + />
); diff --git a/src/chat/demos/icons.tsx b/src/chat/demos/icons.tsx deleted file mode 100644 index 24532c30a..000000000 --- a/src/chat/demos/icons.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { LikeOutlined } from '@ant-design/icons'; -import { Button } from 'antd'; -import { Chat } from 'dt-react-component'; - -import { mockSSE } from './mockSSE'; - -export default function () { - const chat = Chat.useChat(); - const [value, setValue] = useState(''); - - const handleSubmit = (raw: string = value) => { - const val = raw.trim(); - if (chat.loading() || !val) return; - setValue(''); - const promptId = new Date().valueOf().toString(); - const messageId = (new Date().valueOf() + 1).toString(); - chat.prompt.create({ id: promptId, title: val }); - chat.message.create(promptId, { id: messageId, content: '' }); - mockSSE({ - message: val, - onopen() { - chat.start(promptId, messageId); - }, - onmessage(str) { - chat.push(promptId, messageId, str); - }, - onstop() { - chat.close(promptId, messageId); - }, - }); - }; - - useEffect(() => { - chat.conversation.create({ id: new Date().valueOf().toString() }); - }, []); - - return ( -
- }> - - 有什么可以帮忙的? -
- - - } - /> -
- handleSubmit()} - placeholder="请输入想咨询的内容…" - /> - handleSubmit()} - disabled={chat.loading() || !value} - > - - -
-
-
- ); -} diff --git a/src/chat/demos/input.tsx b/src/chat/demos/input.tsx index 5bccd91ab..ff30d89a8 100644 --- a/src/chat/demos/input.tsx +++ b/src/chat/demos/input.tsx @@ -2,12 +2,15 @@ import React, { useState } from 'react'; import { Chat } from 'dt-react-component'; export default function () { - const [value, setValue] = useState(''); + const [value, setValue] = useState(''); return ( console.log('value:', value)} /> ); diff --git a/src/chat/demos/markdown.tsx b/src/chat/demos/markdown.tsx index 081c19170..bc6435fc8 100644 --- a/src/chat/demos/markdown.tsx +++ b/src/chat/demos/markdown.tsx @@ -25,6 +25,8 @@ SELECT * FROM table_name; 2. 有序列表 3. 有序列表 +--- + > 引用 | 表头 | 表头 | 表头 | diff --git a/src/chat/demos/mockSSE.ts b/src/chat/demos/mockSSE.ts index c2204fa8f..f655e87a8 100644 --- a/src/chat/demos/mockSSE.ts +++ b/src/chat/demos/mockSSE.ts @@ -33,6 +33,14 @@ const str = [ `, ]; +const codeBlockStr = ` +[change convert]() in the content. + +\`\`\`sql +select * from table; +\`\`\` +`; + function getRandomLength() { return Math.floor(Math.random() * 10); } @@ -56,6 +64,9 @@ export function mockSSE({ message, onopen, onmessage, onstop }: SSEProps) { onmessage?.(text.slice(point, point + length)); point += length; }, 100); + } else if (message.includes('CodeBlock')) { + onmessage?.(codeBlockStr); + onstop?.(); } else { onmessage?.('根据你的描述暂未检索相关诗词。'); onstop?.(); diff --git a/src/chat/demos/prompt.tsx b/src/chat/demos/prompt.tsx index 617319008..32b6ef29e 100644 --- a/src/chat/demos/prompt.tsx +++ b/src/chat/demos/prompt.tsx @@ -6,7 +6,7 @@ import { Prompt } from 'dt-react-component/chat/entity'; const updateReducer = (num: number): number => (num + 1) % 1_000_000; export default function () { - const [value, setValue] = useState(''); + const [value, setValue] = useState(''); const [, update] = useReducer(updateReducer, 0); const prompt = useRef(new (class extends Prompt {})({ id: '1', title: 'test' })); @@ -23,13 +23,10 @@ export default function () { value={value} onChange={setValue} placeholder="请输入" - onPressEnter={() => value.trim() && setContent(value.trim())} - /> - } - onClick={() => setContent(value.trim())} + button={{ + disabled: !value?.trim(), + }} + onPressEnter={() => value?.trim() && setContent(value.trim())} /> diff --git a/src/chat/demos/properties.tsx b/src/chat/demos/properties.tsx deleted file mode 100644 index f3a04dc44..000000000 --- a/src/chat/demos/properties.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button } from 'antd'; -import { Chat } from 'dt-react-component'; - -const str = ` -[change convert]() in the content. - -\`\`\`sql -select * from table; -\`\`\` -`; - -export default function () { - const chat = Chat.useChat(); - const [value, setValue] = useState(''); - - const [convert, setConvert] = useState(false); - - const handleSubmit = (raw: string = value) => { - const val = raw.trim(); - if (chat.loading() || !val) return; - setValue(''); - const promptId = new Date().valueOf().toString(); - const messageId = (new Date().valueOf() + 1).toString(); - chat.prompt.create({ id: promptId, title: val }); - chat.message.create(promptId, { id: messageId, content: '' }); - - Promise.resolve().then(() => { - chat.start(promptId, messageId); - chat.push(promptId, messageId, str); - chat.close(promptId, messageId); - }); - }; - - useEffect(() => { - chat.conversation.create({ id: new Date().valueOf().toString() }); - }, []); - - return ( -
- ( - - ), - }} - > - - 有什么可以帮忙的? -
- - - } - /> -
- handleSubmit()} - placeholder="请输入想咨询的内容…" - /> - handleSubmit()} - disabled={chat.loading() || !value} - > - - -
-
-
- ); -} diff --git a/src/chat/icon/index.tsx b/src/chat/icon/index.tsx index 3a7ba1c66..edced402c 100644 --- a/src/chat/icon/index.tsx +++ b/src/chat/icon/index.tsx @@ -10,44 +10,6 @@ export interface IconProps extends React.HTMLAttributes { gradient?: boolean; } -/** - * @deprecated 后续迁移至 icon 库 - */ -export function AssistantAvatarIcon({ className, ...rest }: IconProps) { - return ( - - - - - - - - - - - - - ); -} - /** * @deprecated 后续迁移至 icon 库 */ @@ -99,7 +61,7 @@ export function PauseIcon({ className, ...rest }: IconProps) { /** * @deprecated 后续迁移至 icon 库 */ -export function RobotIcon({ className, ...rest }: IconProps) { +export function SendIcon({ className, gradient, ...rest }: IconProps) { return ( @@ -130,32 +91,20 @@ export function RobotIcon({ className, ...rest }: IconProps) { - - - ); -} - -/** - * @deprecated 后续迁移至 icon 库 - */ -export function SendIcon({ className, gradient, ...rest }: IconProps) { - return ( - - - - + + + + + + + ); @@ -263,3 +212,244 @@ export const ShiningIcon = ({ className, ...rest }: IconProps) => { ); }; + +/** + * @deprecated 后续迁移至 icon 库 + */ +export const AIAvatar = ({ className, ...rest }: IconProps) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/chat/index.md b/src/chat/index.md index 22ad76c54..10b913ae5 100644 --- a/src/chat/index.md +++ b/src/chat/index.md @@ -2,8 +2,6 @@ title: Chat 智能问答 group: 组件 toc: content -demo: - cols: 2 --- # 智能问答 @@ -31,8 +29,6 @@ Chat 规范由多个组件复合使用实现落地场景,其中: ## 示例 - - ## API diff --git a/src/chat/index.tsx b/src/chat/index.tsx index f27839245..1b74548e0 100644 --- a/src/chat/index.tsx +++ b/src/chat/index.tsx @@ -13,6 +13,7 @@ import Prompt from './prompt'; import Tag from './tag'; import useChat from './useChat'; import { context, type IChatContext, useContext } from './useContext'; +import Welcome from './welcome'; type IChatProps = IChatContext; @@ -65,6 +66,7 @@ Chat.Message = Message; Chat.Prompt = Prompt; Chat.Content = Content; Chat.Tag = Tag; +Chat.Welcome = Welcome; Chat.Icon = { SendIcon, diff --git a/src/chat/input/index.scss b/src/chat/input/index.scss new file mode 100644 index 000000000..c46ff3764 --- /dev/null +++ b/src/chat/input/index.scss @@ -0,0 +1,37 @@ +.dtc__chat__textarea__container { + width: 100%; + border-radius: 8px; + border: 1px solid #D8DAE2; + background: #FFF; + position: relative; + max-width: 800px; + margin: 0 auto; + .dtc__chat__textarea { + border: none; + background-color: transparent; + color: #3D446E; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + padding: 8px 36px 8px 16px; + &:focus { + box-shadow: none; + } + } + .dtc__chat__textarea__send { + font-size: 24px; + position: absolute; + bottom: 8px; + right: 8px; + &:hover:not(&--disabled) { + &.dtc__icon path { + fill: url(#paint0_linear_3878_6538_hover); + } + } + &--disabled { + cursor: not-allowed; + color: #B1B4C5; + } + } +} diff --git a/src/chat/input/index.tsx b/src/chat/input/index.tsx index 06bc9727c..ef7b0f995 100644 --- a/src/chat/input/index.tsx +++ b/src/chat/input/index.tsx @@ -3,15 +3,25 @@ import { Input as AntdInput } from 'antd'; import { TextAreaProps } from 'antd/lib/input/TextArea'; import classNames from 'classnames'; -interface IInputProps extends Omit { - onChange?: (str: string) => void; +import { SendIcon } from '../icon'; +import './index.scss'; + +interface IInputProps extends Omit { + value?: string; + button?: { + disabled?: boolean; + }; + onChange?: (str?: string) => void; onPressShiftEnter?: TextAreaProps['onPressEnter']; + onSubmit?: (str?: string) => void; } export default function Input({ onChange, onPressEnter, onPressShiftEnter, + onSubmit, + button, className, ...rest }: IInputProps) { @@ -20,24 +30,34 @@ export default function Input({ }; return ( - { - e.persist(); - e.preventDefault(); - if (e.shiftKey) { - onChange?.(rest.value + '\n'); - onPressShiftEnter?.(e); - } else { - onPressEnter?.(e); - } - }} - autoSize={{ - minRows: 1, - maxRows: 8, - }} - /> +
+ { + e.persist(); + e.preventDefault(); + if (e.shiftKey) { + onChange?.(rest.value + '\n'); + onPressShiftEnter?.(e); + } else { + onPressEnter?.(e); + } + }} + autoSize={{ + minRows: 2, + maxRows: 7, + }} + /> + !button?.disabled && onSubmit?.(rest.value)} + /> +
); } diff --git a/src/chat/markdown/index.scss b/src/chat/markdown/index.scss index 265fd4dd9..49a54fde1 100644 --- a/src/chat/markdown/index.scss +++ b/src/chat/markdown/index.scss @@ -81,6 +81,10 @@ border: 1px solid rgba(6, 14, 26, 0.08); word-break: keep-all; } + &__hr { + margin: 8px; + border-top: none; + } > *:last-child { margin-bottom: 0; } diff --git a/src/chat/markdown/index.tsx b/src/chat/markdown/index.tsx index 1f63d201c..1718e8870 100644 --- a/src/chat/markdown/index.tsx +++ b/src/chat/markdown/index.tsx @@ -45,6 +45,9 @@ export default memo( pre({ children }) { return {children}; }, + hr() { + return
; + }, ...components, }} {...rest} diff --git a/src/chat/message/index.scss b/src/chat/message/index.scss index 4dc042931..8e74318c4 100644 --- a/src/chat/message/index.scss +++ b/src/chat/message/index.scss @@ -74,6 +74,7 @@ &__icon { cursor: pointer; color: #8B8FA8; + font-size: 16px; &:hover { color: #0A67F2; } diff --git a/src/chat/message/index.tsx b/src/chat/message/index.tsx index 51dc3306c..a01a77f23 100644 --- a/src/chat/message/index.tsx +++ b/src/chat/message/index.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames'; import Copy from '../../copy'; import useIntersectionObserver from '../../useIntersectionObserver'; import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity'; -import { AssistantAvatarIcon, CopyIcon, PauseIcon, ReloadIcon } from '../icon'; +import { AIAvatar, CopyIcon, PauseIcon, ReloadIcon } from '../icon'; import Loading from '../loading'; import Markdown from '../markdown'; import Pagination from '../pagination'; @@ -108,26 +108,18 @@ export default function Message({ const renderCopyIcon = () => { if (copyInfo.disabled) return null; - const { formatText, style, ...rest } = copyInfo.options; + const { formatText, ...rest } = copyInfo.options; const text = formatText?.(record?.content) ?? record?.content; if (!text) return null; - return ( - } - text={text} - style={{ fontSize: 16, ...style }} - {...rest} - /> - ); + return } text={text} {...rest} />; }; return (
- +
{!typing && !loading && ( - + {renderCopyIcon()} {typeof messageIcons === 'function' ? messageIcons(record, prompt) @@ -192,7 +184,7 @@ export default function Message({ className="dtc__message__icon" onClick={() => onRegenerate?.(record)} > - + )} diff --git a/src/chat/welcome/index.scss b/src/chat/welcome/index.scss new file mode 100644 index 000000000..5d67e64ea --- /dev/null +++ b/src/chat/welcome/index.scss @@ -0,0 +1,34 @@ +$primaryGradient: #00BAC6 0%, #0067FF 50%, #450FDE 100%; + +.dtc__welcome { + overflow: hidden; + color: #FFF; + background: linear-gradient(110deg, $primaryGradient) border-box; + border: 1px solid transparent; + &__content { + padding: 12px; + position: relative; + z-index: 2; + background: + linear-gradient( + 110deg, + rgba(0, 186, 198, 0.05) 0%, + rgba(0, 103, 255, 0.05) 50%, + rgba(69, 15, 222, 0.05) 100% + ), + #FFF; + } + &__title { + color: #3D446E; + font-size: 20px; + font-weight: 500; + line-height: 28px; + } + &__description { + color: #64698B; + font-size: 12px; + font-weight: 400; + line-height: 20px; + margin-top: 4px; + } +} diff --git a/src/chat/welcome/index.tsx b/src/chat/welcome/index.tsx new file mode 100644 index 000000000..a8c65630e --- /dev/null +++ b/src/chat/welcome/index.tsx @@ -0,0 +1,34 @@ +import React, { CSSProperties } from 'react'; +import classNames from 'classnames'; + +import Flex from '../../flex'; +import { AIAvatar } from '../icon'; +import './index.scss'; + +export interface IWelcomeProps { + title: React.ReactNode; + description?: React.ReactNode; + icon?: React.ReactNode; + className?: string; + style?: CSSProperties; +} + +export default function Welcome({ + icon = , + title, + description, + className, + style, +}: IWelcomeProps) { + return ( +
+
+ + {icon} + {title} + +
{description}
+
+
+ ); +}