diff --git a/src/components/chart-minimize/chart-minimize.scss b/src/components/chart-minimize/chart-minimize.scss index 6e0560e1a4a153a4d5a84252df21fc165b552def..722b86b1e685b45e979a4b25b783cb8c8b4c56a2 100644 --- a/src/components/chart-minimize/chart-minimize.scss +++ b/src/components/chart-minimize/chart-minimize.scss @@ -28,8 +28,8 @@ } @include e(output) { - font-size: 12px; @include m(popover) { + font-size: 12px; position: absolute; bottom: 60px; left: 50%; @@ -39,13 +39,14 @@ border: 1px solid #{getCssVar('ai-chat', 'border-color')}; border-radius: 8px; padding: 8px; - max-width: 200px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); display: block; .typewriter { direction: rtl; overflow: hidden; + max-width: 200px; + width: fit-content; white-space: nowrap; text-overflow: ellipsis; animation: typing 3s steps(40, end); diff --git a/src/components/chart-minimize/chart-minimize.tsx b/src/components/chart-minimize/chart-minimize.tsx index 7b44449fc25644f51d2b9aacc52f56a220a29ade..57d9dca44e96d4beefd8d8b37f0ca5998bb29cec 100644 --- a/src/components/chart-minimize/chart-minimize.tsx +++ b/src/components/chart-minimize/chart-minimize.tsx @@ -1,9 +1,9 @@ -import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { useComputed } from '@preact/signals'; import { Namespace, isWithinBounds, limitDraggable } from '../../utils'; import { AiChatController } from '../../controller'; -import { AISvg } from '../../icons'; import { AIChatConst } from '../../constants'; +import { AISvg } from '../../icons'; import './chart-minimize.scss'; export interface ChatMinimizeProps { @@ -44,6 +44,7 @@ export const ChatMinimize = (props: ChatMinimizeProps) => { const [displayedContent, setDisplayedContent] = useState(''); // 当前显示的字符索引 const [currentIndex, setCurrentIndex] = useState(0); + /** * 是否在拖拽中 */ @@ -52,81 +53,84 @@ export const ChatMinimize = (props: ChatMinimizeProps) => { /** * 最小化样式 */ - const style = useRef({ + const style = { x: (window.innerWidth - 86) / window.innerWidth, y: (window.innerHeight - 86) / window.innerHeight, - }); + }; /** - * 当前消息 + * AI是否在输出中 * */ - const message = useComputed(() => { - return ( + const isOutput = useComputed(() => { + const message = props.controller.messages.value[ props.controller.messages.value.length - 1 - ] || undefined + ]; + if (!message) return false; + return ( + message.role === 'ASSISTANT' && + message.state === 20 && + message.completed !== true ); }); /** - * 是否在输出中 + * 解析内容 * + * @param {string} text + * @return {*} */ - const isOutput = useMemo(() => { - return message.value?.state === 20 && message.value?.completed !== true; - }, [message.value]); - const parseThinkContent = (text: string) => { const openThinkIndex = text.indexOf(''); const closeThinkIndex = text.indexOf(''); let thoughtContent = ''; let answerContent = ''; - let isThoughtCompleted = false; if (closeThinkIndex === -1) { - isThoughtCompleted = false; thoughtContent = text.slice(openThinkIndex + 7); answerContent = ''; } else { - isThoughtCompleted = true; thoughtContent = text.slice(openThinkIndex + 7, closeThinkIndex); answerContent = text.slice(closeThinkIndex + 8); } - return { isThoughtCompleted, thoughtContent, answerContent }; + return { thoughtContent, answerContent }; }; /** * 消息内容 * */ - const msgContent = useMemo(() => { - let content = ''; - if (isOutput) { - if (message.value.content.indexOf('') !== -1) { - const { thoughtContent, answerContent } = parseThinkContent( - message.value.content, - ); - content = thoughtContent + answerContent; - } else { - content = message.value?.content; - } - } else { + const msgContent = useComputed(() => { + let content: string = ''; + if (!isOutput.value) { // 清空显示内容 setDisplayedContent(''); // 重置字符索引 setCurrentIndex(0); + return content; + } + const message = + props.controller.messages.value[ + props.controller.messages.value.length - 1 + ]; + content = message.content; + if (message.content.indexOf('') !== -1) { + const { thoughtContent, answerContent } = parseThinkContent( + message.content, + ); + content = thoughtContent + answerContent; } return content; - }, [message.value]); + }); /** * 设置样式 */ const setStyle = (): void => { Object.assign(ref.current!.style, { - left: `${style.current.x * 100}%`, - top: `${style.current.y * 100}%`, + left: `${style.x * 100}%`, + top: `${style.y * 100}%`, }); localStorage.setItem( AIChatConst.MINIMIZE_STYLY_CHCHE, @@ -157,7 +161,7 @@ export const ChatMinimize = (props: ChatMinimizeProps) => { width, height, ); - style.current = { x, y }; + Object.assign(style, { x, y }); requestAnimationFrame(() => { setStyle(); }); @@ -191,23 +195,23 @@ export const ChatMinimize = (props: ChatMinimizeProps) => { const cache = localStorage.getItem(AIChatConst.MINIMIZE_STYLY_CHCHE); if (cache) { const data = JSON.parse(cache); - if (isWithinBounds(data)) style.current = data; + if (isWithinBounds(data)) Object.assign(style, data); } setStyle(); registerDragMinmize(); }, []); - // 逐个字符显示 useEffect(() => { - if (currentIndex < msgContent.length) { + // 监听消息内容,以打字机的方式展示字符串类容 + if (currentIndex < msgContent.value.length) { const timer = setTimeout(() => { - setDisplayedContent(prev => prev + msgContent[currentIndex]); + setDisplayedContent(prev => prev + msgContent.value[currentIndex]); setCurrentIndex(prev => prev + 1); }, 100); // 每个字符的显示间隔时间(100ms) - - return () => clearTimeout(timer); // 清理定时器 + // 清理定时器 + return () => clearTimeout(timer); } - }, [currentIndex, msgContent]); + }, [currentIndex, msgContent.value]); return (
{ className={`${ns.b()} ${ns.is('hidden', !props.isMinimize)}`} onClick={handleClick} > - {isOutput ? ( + {isOutput.value ? (
输出中 {displayedContent && ( diff --git a/src/components/chat-back-bottom/chat-back-bottom.tsx b/src/components/chat-back-bottom/chat-back-bottom.tsx index e8264e47b6a2cab98f473ba2214d08ddfe1238b4..d993736ea3062223ead88cca0abb48f152facf1f 100644 --- a/src/components/chat-back-bottom/chat-back-bottom.tsx +++ b/src/components/chat-back-bottom/chat-back-bottom.tsx @@ -32,6 +32,12 @@ export interface ChatBackBottomProps { * @memberof ChatBackBottomProps */ visibilityHeight?: number; + /** + * 点击事件 + * + * @memberof ChatBackBottomProps + */ + onClick?: () => void; } export function throttle( @@ -77,6 +83,7 @@ export const ChatBackBottom = (props: ChatBackBottomProps) => { top: container.value.scrollHeight, behavior: 'smooth', }); + props.onClick?.(); } }; diff --git a/src/components/chat-message-item/error-message/error-message.scss b/src/components/chat-message-item/error-message/error-message.scss index 22455b0d23c0f4308502a05321eed70567a14d0c..2f1f3e358c0c6b3e6f1608c2004351d8ab1339ae 100644 --- a/src/components/chat-message-item/error-message/error-message.scss +++ b/src/components/chat-message-item/error-message/error-message.scss @@ -4,7 +4,7 @@ @include e(content) { width: fit-content; - padding: 6px; + padding: 10px 16px; color: #fff; background-color: red; border-radius: #{getCssVar('ai-chat', 'border-radius')}; 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 bf0cf3917a8c66fefc4345ebbb2e97a97b5116f9..82b1501569058c73ce7dcf1dedb25448d5797d37 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.scss +++ b/src/components/chat-message-item/markdown-message/markdown-message.scss @@ -1,5 +1,5 @@ @include b(markdown-message) { - border-radius: 5px; + padding: 6px 10px; .cherry { border-radius: #{getCssVar('ai-chat', 'border-radius')}; diff --git a/src/components/chat-message-item/unknown-message/unknown-message.scss b/src/components/chat-message-item/unknown-message/unknown-message.scss index a2637afb298c769ca422f4a9706abaeeddbda54a..cf53c26e08741bb677de1c581f458dfa649dbc5b 100644 --- a/src/components/chat-message-item/unknown-message/unknown-message.scss +++ b/src/components/chat-message-item/unknown-message/unknown-message.scss @@ -1,10 +1,12 @@ @include b(unknown-message) { + display: flex; padding: 6px 10px; - text-align: right; + justify-content: end; @include e(content) { - padding: 6px; + padding: 10px 16px; color: #000; + width: max-content; background-color: #ccc; border-radius: #{getCssVar('ai-chat', 'border-radius')}; } diff --git a/src/components/chat-message-item/unknown-message/unknown-message.tsx b/src/components/chat-message-item/unknown-message/unknown-message.tsx index acced804ecfcde743a2930aab497c25c25b21ff6..99ed2ff86ea443e9ca47c40f275f2f4ebcf970d0 100644 --- a/src/components/chat-message-item/unknown-message/unknown-message.tsx +++ b/src/components/chat-message-item/unknown-message/unknown-message.tsx @@ -19,9 +19,9 @@ const ns = new Namespace('unknown-message'); export const UnknownMessage = (props: UnknownMessageProps) => { return (
- +
暂未支持的消息类型: {props.message.type} - +
); }; diff --git a/src/components/chat-message-item/user-message/user-message.scss b/src/components/chat-message-item/user-message/user-message.scss index be24a68012a927ed77e061538e37d84249e28ccb..80c64a2437b12307a5f0bbfae31b9c11db5687e3 100644 --- a/src/components/chat-message-item/user-message/user-message.scss +++ b/src/components/chat-message-item/user-message/user-message.scss @@ -7,57 +7,119 @@ display: flex; justify-content: end; max-width: 100%; - padding: 10px; font-size: 14px; text-align: left; word-break: break-word; user-select: text; - transition: all .3s ease; - @include m('body'){ + transition: all 0.3s ease; + @include m('body') { width: fit-content; - padding:0 10px; + padding: 10px 16px; margin: 0; font-size: 14px; line-height: 1.5; color: var(--color-fg-default); word-wrap: break-word; - background: #E7F8FF; + background: #e7f8ff; border-radius: #{getCssVar('ai-chat', 'border-radius')}; text-size-adjust: 100%; + background-color: getCssVar('ai-chat', 'background-color-light'); + border-radius: getCssVar('ai-chat', 'border-radius'); - &::before{ + &::before { display: table; - content: ""; + content: ''; } - &::after{ + &::after { display: table; clear: both; - content: ""; - } - - >p{ - margin: 8px; + content: ''; } } - @include m('material'){ - display: flex; - flex-flow: row-reverse wrap; + @include m('material') { gap: 8px; + display: flex; align-items: center; + flex-flow: row-reverse wrap; + margin-bottom: 8px; + } + .cherry { + min-height: 0px; + border-radius: #{getCssVar('ai-chat', 'border-radius')}; + box-shadow: none; + + *::-webkit-scrollbar { + width: getCssVar('ai-chat', 'scroll-bar-width'); + height: getCssVar('ai-chat', 'scroll-bar-height'); + } + + *::-webkit-scrollbar-thumb { + background-color: getCssVar(color, fill, 2); + border-radius: getCssVar('ai-chat', 'scroll-bar-radius'); + } + + *::-webkit-scrollbar-thumb:hover { + background-color: getCssVar(color, fill, 2); + } + } + + .cherry-markdown p { + margin: 0; + } + + .cherry-markdown pre { + background-color: getCssVar(ai-chat, background, color); + } + + .cherry-markdown pre { + background-color: getCssVar(ai-chat, background, color); + } + + .cherry-previewer { + padding: 0px; + border: 0; + color: getCssVar(ai-chat, color); + background-color: getCssVar('ai-chat', 'background-color-light'); + + figure { + max-width: 1200px; + + > svg { + width: 100%; + min-height: 100px; + } + } + + div[data-type='codeBlock'] { + position: relative !important; + display: flex; + padding-top: 24px; + + .cherry-edit-code-block, + .cherry-copy-code-block { + position: absolute; + top: 0; + } + } + } + + .anchor { + display: none !important; } } - @include e('user-header'){ + @include e('user-header') { + width: 100%; + height: 32px; display: flex; + margin-bottom: 8px; align-items: center; justify-content: end; - width: 100%; - height: 32px; } - @include e('user'){ + @include e('user') { padding: 6px; font-size: 14px; font-weight: 800; diff --git a/src/components/chat-message-item/user-message/user-message.tsx b/src/components/chat-message-item/user-message/user-message.tsx index f127ec0f324550b0d30e1e4c49a8de9796ce8636..40edb1a146b0a9a1e3a0b1743c619f64cf1d1298 100644 --- a/src/components/chat-message-item/user-message/user-message.tsx +++ b/src/components/chat-message-item/user-message/user-message.tsx @@ -1,10 +1,12 @@ import { VNode } from 'preact'; -import { useComputed } from '@preact/signals'; +import { useComputed, useSignal } from '@preact/signals'; +import Cherry from 'cherry-markdown'; +import { useEffect } from 'preact/hooks'; import { IChatMessage } from '../../../interface'; -import { MaterialResourceParser, Namespace } from '../../../utils'; +import { MaterialResourceParser, Namespace, createUUID } from '../../../utils'; import { AiChatController } from '../../../controller'; -import './user-message.scss'; import { ChatInputMaterialtem } from '../../chat-input-material-item/chat-input-material-item'; +import './user-message.scss'; export interface UserMessageProps { controller: AiChatController; @@ -30,11 +32,26 @@ export interface UserMessageProps { const ns = new Namespace('user-message-question'); export const UserMessage = (props: UserMessageProps) => { + const uuid = useSignal(createUUID()); + + const cherry = useSignal(null); + const content = useComputed(() => props.message.content); + const materialResult = useComputed(() => { return MaterialResourceParser.parseMixedContent(content.value); }); + useEffect(() => { + cherry.value = new Cherry({ + id: uuid, + value: materialResult.value.remainingText || '', + editor: { + defaultModel: 'previewOnly', + }, + }); + }, [materialResult.value.remainingText]); + return (
@@ -42,10 +59,10 @@ export const UserMessage = (props: UserMessageProps) => {
-
-

- {materialResult.value.hasResources && - materialResult.value.resources.map(resource => { +

+ {materialResult.value.hasResources && ( +
+ {materialResult.value.resources.map(resource => { return ( { > ); })} -

-

- {materialResult.value.remainingText} -

+
+ )} +
+
+
diff --git a/src/components/chat-messages/chat-messages.tsx b/src/components/chat-messages/chat-messages.tsx index 5093671ff168f205da388669fea9e4215af28a7a..1481ec55ab394849e20c9e9904227403b54c01dc 100644 --- a/src/components/chat-messages/chat-messages.tsx +++ b/src/components/chat-messages/chat-messages.tsx @@ -1,5 +1,4 @@ -import { useRef } from 'preact/hooks'; -import { useSignalEffect } from '@preact/signals'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { Namespace } from '../../utils'; import { AiChatController } from '../../controller'; import { IChatToolbarItem } from '../../interface'; @@ -31,52 +30,54 @@ const ns = new Namespace('chat-messages'); export const ChatMessages = (props: ChatMessageProps) => { const ref = useRef(null); - + // 是否自动滚动 + const [isAutoScroll, setIsAutoScroll] = useState(true); + // 标志位 + const isScrollingAutomatically = useRef(false); const messages = props.controller.messages; - // 用于标记用户是否手动滚动到了上方 - const isUserScrolledUp = useRef(false); - const setScrollTo = () => { + // 滚动到底部 + const scrollToBottom = () => { const container = ref.current; if (!container) return; - // 如果用户没有手动滚动到上方,则自动滚动到底部 - if (!isUserScrolledUp.current) { - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth', - }); - } + // 设置标志位,表示当前是自动滚动 + isScrollingAutomatically.current = true; + container.scrollTo({ + top: container.scrollHeight, + behavior: 'auto', + }); + setTimeout(() => { + isScrollingAutomatically.current = false; + }, 500); }; - useSignalEffect(() => { - if (messages.value.length === 0) return; - // 如果最后一个信息是用户提问,则恢复自动滚动 - if (messages.value[messages.value.length - 1].role === 'USER') { - isUserScrolledUp.current = false; - setTimeout(() => { - setScrollTo(); - }, 100); - } else { - setScrollTo(); - } - }); + useEffect(() => { + // 如果是自动滚动模式,则滚动到底部 + if (isAutoScroll) scrollToBottom(); + }, [messages.value]); - // 监听滚动事件,判断用户是否手动滚动到了上方 + // 监听滚动事件 const handleScroll = () => { - const container = ref.current; - if (!container) return; - // 判断用户是否滚动到了上方 - const { scrollTop, scrollHeight, clientHeight } = container; - const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 50; - // 如果用户滚动到了底部,恢复自动滚动 - if (isNearBottom) { - isUserScrolledUp.current = false; - } else { - // 否则标记为用户手动滚动到了上方 - isUserScrolledUp.current = true; + // 如果是自动滚动触发的,忽略此次事件 + if (isScrollingAutomatically.current) return; + if (ref.current) { + const { scrollTop, scrollHeight, clientHeight } = ref.current; + // 接近底部的阈值 + const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 50; + // 如果接近底部,则设置为自动滚动模式;否则设置为手动滚动模式 + setIsAutoScroll(isNearBottom); } }; + /** + * 处理回到底部 + * + */ + const handleBackBottom = () => { + isScrollingAutomatically.current = true; + setIsAutoScroll(true); + }; + return (
{messages.value.map(message => { @@ -97,7 +98,12 @@ export const ChatMessages = (props: ChatMessageProps) => { ) : null; })} - +
); };