diff --git a/src/components/chat-container/chat-container.scss b/src/components/chat-container/chat-container.scss index fea53f14cef2a6c0ef96545db793d4ebccdfb60c..67db77c233ea5de83493febd0efd82aa1f3d23c7 100644 --- a/src/components/chat-container/chat-container.scss +++ b/src/components/chat-container/chat-container.scss @@ -4,6 +4,7 @@ $ai-chat: ( 'icon-color': #3b3b3b, 'border-color': #e5e5e5, 'background-color': #fff, + 'background-color-light': #f8fafb, // 禁用态 'disabled-color': rgb(59 59 59 / 60%), // 悬浮态 diff --git a/src/components/chat-message-item/markdown-message/markdown-message.scss b/src/components/chat-message-item/markdown-message/markdown-message.scss index c21199e1f1b64fd87ce917adca93c00edca187d9..f5f5077df0af5f56913a26fcc47197bad273aa5f 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.scss +++ b/src/components/chat-message-item/markdown-message/markdown-message.scss @@ -9,9 +9,10 @@ .cherry-previewer { padding: 8px; border: 0; + figure > svg { - min-height: 100px; width: 100%; + min-height: 100px; } } @@ -54,6 +55,18 @@ font-weight: 800; color: #{getCssVar('ai-chat', 'color')}; } + + @include e(timeout) { + display: flex; + align-items: center; + padding: 0 8px; + } +} + +@include b(markdown-message-content) { + padding: 10px 16px; + background-color: getCssVar('ai-chat', 'background-color-light'); + border-radius: getCssVar('ai-chat', 'border-radius'); } @keyframes circle { diff --git a/src/components/chat-message-item/markdown-message/markdown-message.tsx b/src/components/chat-message-item/markdown-message/markdown-message.tsx index 7415f8f8c188ed1896380adc7357c79d80171ba6..a55cbde7fe00884cb2d29754709125a0adca7c8f 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.tsx +++ b/src/components/chat-message-item/markdown-message/markdown-message.tsx @@ -3,9 +3,11 @@ import Cherry from 'cherry-markdown'; import { useSignal } from '@preact/signals'; import { useEffect, useMemo } from 'preact/hooks'; import { Namespace, createUUID } from '../../../utils'; -import { IChatMessage } from '../../../interface'; +import { IChatMessage, IChatThoughtChain } from '../../../interface'; import { AiChatController } from '../../../controller'; import './markdown-message.scss'; +import { ChatThoughtChain } from '../../chat-thought-chain/chat-thought-chain'; +import { CheckMarkCircleSvg, LoadingSvg } from '../../../icons'; export interface MarkdownMessageProps { /** @@ -88,14 +90,10 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { return message.state === 20 && message.completed === true; }, [message.state, message.completed]); - const thoughtChain = useSignal<{ - title: string; - description: string; - icon: string; - }>({ + const thoughtChain = useSignal({ title: '思考过程', description: '', - icon: '思考中', + icon: , }); /** @@ -132,7 +130,12 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { const { isThoughtCompleted, thoughtContent, answerContent } = parseThinkContent(message.content); if (isThoughtCompleted) { - thoughtChain.value.icon = isThoughtCompleted ? '思考完成' : '思考中'; + thoughtChain.value.icon = isThoughtCompleted ? ( + + ) : ( + + ); + thoughtChain.value.done = isThoughtCompleted; } if (thoughtContent) { thoughtChain.value.description = thoughtContent; @@ -154,7 +157,12 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { thoughtChain.value = { title: '思考过程', description: thoughtContent || '', - icon: isThoughtCompleted ? '思考完成' : '思考中', + icon: isThoughtCompleted ? ( + + ) : ( + + ), + done: isThoughtCompleted, }; if (answerContent) { content = answerContent; @@ -176,10 +184,12 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => {
AI
{props.children} - {isTimeOut ?
请求超时
: null} + {isTimeOut ? ( +
请求超时
+ ) : null}
-
{thoughtChain.value.description}
+
diff --git a/src/components/chat-thought-chain/chat-thought-chain.scss b/src/components/chat-thought-chain/chat-thought-chain.scss new file mode 100644 index 0000000000000000000000000000000000000000..98e741c840bf6af03a335dcccd2535005f8c7e3e --- /dev/null +++ b/src/components/chat-thought-chain/chat-thought-chain.scss @@ -0,0 +1,132 @@ +$chat-thought-chain: ( + font-size: 12px, + header-height: 32px, + color-2: var(--ibiz-ai-chat-color-2), + bg: var(--ibiz-ai-chat-hover-background-color), + bg-2: var(--ibiz-ai-chat-hover-background-color-2), + border-radius: var(--ibiz-ai-chat-border-radius), + hover-bg-color: var(--ibiz-ai-chat-hover-background-color), + border: var(--ibiz-ai-chat-hover-background-color), +); + +@include b(chat-thought-chain) { + @include set-component-css-var('chat-thought-chain', $chat-thought-chain); + + display: flex; + flex-direction: column; + padding: 8px 12px; + font-size: getCssVar(chat-thought-chain, font-size); + background-color: getCssVar(chat-thought-chain, bg); + border-radius: getCssVar(chat-thought-chain, border-radius); + + @include e(item) { + display: flex; + + @include when(collapsed) { + &:last-child { + .#{bem(chat-thought-chain, item-icon)} { + &::after { + display: none; + } + } + } + .#{bem(chat-thought-chain, item-title)} { + .#{bem(chat-thought-chain, icon)} { + transform: rotate(180deg); + } + } + .#{bem(chat-thought-chain, item-description)} { + height: 0; + } + } + } + + .#{bem(chat-thought-chain, item-icon)} { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + width: 18px; + padding-top: 8px; + + span { + display: flex; + flex: none; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-bottom: 8px; + font-size: 10px; + color: getCssVar(chat-thought-chain, color, 2);; + background: getCssVar(chat-thought-chain, bg, 2); + border-radius: 50%; + } + + svg { + width: 16px; + height: 16px; + margin-bottom: 8px; + } + + &::after { + width: 1px; + height: 100%; + content: ""; + background: getCssVar(chat-thought-chain, border); + border-radius: 13px; + transition: .3s background cubic-bezier(.4,0,.2,1); + } + } + + @include e(item-content) { + flex: auto; + padding-left: 4px; + } + @include e(item-title) { + position: relative; + display: flex; + align-items: center; + height: getCssVar(chat-thought-chain, header-height); + padding: 4px 4px 4px 6px; + cursor: pointer; + transition: .3s background-color; + + .#{bem(chat-thought-chain, icon)} { + position: absolute; + right: 8px; + flex-shrink: 0; + font-size: 1em; + cursor: pointer; + transition: .3s transform; + } + + &:hover { + background-color: getCssVar(chat-thought-chain, hover, bg, color); + border-radius: 8px; + } + } + @include e(item-description) { + height: 100%; + overflow: hidden; + color: var(--ibiz-color-text-2); + transition: height .3s cubic-bezier(.4,0,.2,1),opacity .3s cubic-bezier(.4,0,.2,1); + } + + // 单节点展示 + @include when(single) { + .#{bem(chat-thought-chain, item-icon)} { + &::after { + display: none; + } + } + } +} + +@keyframes loading-animation { + 75%,100% { + opacity: 0; + transform: scale(2) + } +} \ No newline at end of file diff --git a/src/components/chat-thought-chain/chat-thought-chain.tsx b/src/components/chat-thought-chain/chat-thought-chain.tsx new file mode 100644 index 0000000000000000000000000000000000000000..50a3d7a11e543708e6971154a01cc4dc2cfdc4f5 --- /dev/null +++ b/src/components/chat-thought-chain/chat-thought-chain.tsx @@ -0,0 +1,81 @@ +import { useSignal } from '@preact/signals'; +import { useEffect } from 'preact/hooks'; +import { Namespace } from '../../utils'; +import { IChatThoughtChain } from '../../interface'; +import { ChevronDownSvg } from '../../icons'; +import './chat-thought-chain.scss'; + +export interface ChatThoughtChainProps { + /** + * @description AI聊天思维链 + * @type {IChatThoughtChain[]} + * @memberof ChatThoughtChainProps + */ + items: IChatThoughtChain[]; +} + +export const ChatThoughtChain = (props: ChatThoughtChainProps) => { + const { items } = props; + + const collapseIndex = useSignal([]); + + const ns = new Namespace('chat-thought-chain'); + + const nodes = useSignal([]); + + useEffect(() => { + nodes.value = items.filter(item => item.description); + if (nodes.value.length > 0) { + nodes.value.forEach((item, index) => { + if (item.done) { + collapseIndex.value = [...collapseIndex.value, index]; + } + }); + } + }, [items]); + + const onCollapse = (index: number) => { + if (collapseIndex.value.includes(index)) { + collapseIndex.value = collapseIndex.value.filter( + (i: number) => i !== index, + ); + } else { + collapseIndex.value = [...collapseIndex.value, index]; + } + }; + + if (nodes.value.length === 0) { + return null; + } + + return ( +
+ {nodes.value.map((item, index) => { + if (!item.description) { + return; + } + const collapsed = collapseIndex.value.includes(index); + return ( +
+
+ {item.icon || {index}} +
+
+
onCollapse(index)} + > + {item.title} + +
+
{item.description}
+
+
+ ); + })} +
+ ); +}; diff --git a/src/icons/checkmark-circle-svg.tsx b/src/icons/checkmark-circle-svg.tsx new file mode 100644 index 0000000000000000000000000000000000000000..840c395131ee911e8edcbd585ba5caa080c4f05c --- /dev/null +++ b/src/icons/checkmark-circle-svg.tsx @@ -0,0 +1,10 @@ +// 完成圆圈的图标 +export const CheckMarkCircleSvg = (props: { className?: string }) => ( + + + +); diff --git a/src/icons/chevron-down-svg.tsx b/src/icons/chevron-down-svg.tsx new file mode 100644 index 0000000000000000000000000000000000000000..30e3c5cdb99a2b90227fa1a140d05da2c5e3df60 --- /dev/null +++ b/src/icons/chevron-down-svg.tsx @@ -0,0 +1,17 @@ +// 下箭头图标 +export const ChevronDownSvg = (props: { className?: string }) => ( + + + +); diff --git a/src/icons/index.ts b/src/icons/index.ts index 0bb3e4f1e1f128ab9d30890daa684b5449354972..ee077058ba9d38a3d28ba69b114023f0bb8b8ee5 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -14,3 +14,6 @@ export { RecordingSvg } from './recording-svg'; export { MoreSvg } from './more-svg'; export { LinkSvg } from './link-svg'; export { RemoveSvg } from './remove-svg'; +export { ChevronDownSvg } from './chevron-down-svg'; +export { CheckMarkCircleSvg } from './checkmark-circle-svg'; +export { LoadingSvg } from './loading-svg'; diff --git a/src/icons/loading-svg.tsx b/src/icons/loading-svg.tsx new file mode 100644 index 0000000000000000000000000000000000000000..55815558b363e8cb01dab1e2f88ee1a9c8b551f3 --- /dev/null +++ b/src/icons/loading-svg.tsx @@ -0,0 +1,29 @@ +export const LoadingSvg = (props: { className?: string }) => ( + + + + + +); diff --git a/src/interface/i-chat-thought-chain/i-chat-thought-chain.ts b/src/interface/i-chat-thought-chain/i-chat-thought-chain.ts new file mode 100644 index 0000000000000000000000000000000000000000..41a954bcaa9442194401b6bee09f947ebdffff21 --- /dev/null +++ b/src/interface/i-chat-thought-chain/i-chat-thought-chain.ts @@ -0,0 +1,38 @@ +/** + * @description AI聊天思维链 + * @export + * @interface IChatThoughtChain + */ +export interface IChatThoughtChain { + /** + * 消息标识 + * + * @author chitanda + * @date 2023-09-05 15:09:43 + * @type {string} + */ + title: string; + + /** + * 消息名称 + * + * @author chitanda + * @date 2023-09-05 15:09:49 + * @type {string} + */ + description: string; + + /** + * @description 图标 + * @type {React.ReactNode} + * @memberof IChatThoughtChain + */ + icon?: React.ReactNode; + + /** + * @description 是否完成 + * @type {boolean} + * @memberof IChatThoughtChain + */ + done?: boolean; +} diff --git a/src/interface/index.ts b/src/interface/index.ts index 89a11d525216d9cda849877ee014fb77fe5d6d10..5db54576e614358dd9ba330bd5e1eca7c085026b 100644 --- a/src/interface/index.ts +++ b/src/interface/index.ts @@ -5,3 +5,4 @@ export type { IPortalAsyncAction } from './i-portal-async-action/i-portal-async- export type { IChatToolbarItem } from './i-chat-toolbar-item/i-chat-toolbar-item'; export type { ITopic, ITopicOptions } from './i-topic-options/i-topic-options'; export type { IContainerOptions } from './i-container-options/i-container-options'; +export type { IChatThoughtChain } from './i-chat-thought-chain/i-chat-thought-chain';