From 8dc664482298b746523c9a537ac3927fbcae548b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=94=BF=E6=9D=83?= <1978141412@qq.com> Date: Fri, 7 Mar 2025 18:37:44 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=BA=95=E9=83=A8=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat-back-bottom/chat-back-bottom.scss | 26 ++++ .../chat-back-bottom/chat-back-bottom.tsx | 112 ++++++++++++++++++ .../chat-container/chat-container.scss | 3 +- .../chat-messages/chat-messages.tsx | 2 + 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/components/chat-back-bottom/chat-back-bottom.scss create mode 100644 src/components/chat-back-bottom/chat-back-bottom.tsx diff --git a/src/components/chat-back-bottom/chat-back-bottom.scss b/src/components/chat-back-bottom/chat-back-bottom.scss new file mode 100644 index 0000000..b8435bb --- /dev/null +++ b/src/components/chat-back-bottom/chat-back-bottom.scss @@ -0,0 +1,26 @@ +@include b(chat-back-bottom) { + position: absolute; + z-index: 5; + display: none; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + font-size: getCssVar(font-size, header, 4); + cursor: pointer; + background-color: getCssVar(ai-chat, background, color, 2); + border-radius: getCssVar(border-radius, circle); + box-shadow: getCssVar(shadow, elevated); + + &:hover { + background-color: getCssVar(ai-chat, hover, background, color, 2); + } + + @include e(icon) { + font-size: 20px; + } + + @include when(visible) { + display: flex; + } +} \ No newline at end of file diff --git a/src/components/chat-back-bottom/chat-back-bottom.tsx b/src/components/chat-back-bottom/chat-back-bottom.tsx new file mode 100644 index 0000000..3aeaa7a --- /dev/null +++ b/src/components/chat-back-bottom/chat-back-bottom.tsx @@ -0,0 +1,112 @@ +import { useSignal } from '@preact/signals'; +import { useEffect, useMemo } from 'preact/hooks'; +import { Namespace } from '../../utils'; +import { ChevronDownSvg } from '../../icons'; +import './chat-back-bottom.scss'; + +export interface ChatBackBottomProps { + /** + * @description 右侧距离 + * @type {number} + * @memberof ChatBackBottomProps + */ + right: number; + + /** + * @description 底部距离 + * @type {number} + * @memberof ChatBackBottomProps + */ + bottom: number; + + /** + * @description 滚动目标 + * @type {string} + * @memberof ChatBackBottomProps + */ + target: string; + + /** + * @description 滚动高度达到此参数值才出现 + * @type {number} + * @memberof ChatBackBottomProps + */ + visibilityHeight?: number; +} + +export function throttle( + fn: (...args: unknown[]) => void | Promise, + wait: number, +): (...args: unknown[]) => void { + let timer: unknown = null; + return function (this: unknown, ...args: unknown[]): void { + if (!timer) { + timer = setTimeout(() => { + fn.apply(this, args); + timer = null; + }, wait); + } + }; +} + +export const ChatBackBottom = (props: ChatBackBottomProps) => { + // 是否显示 + const visible = useSignal(false); + + // 按钮样式 + const buttonStyle = useSignal({}); + + const ns = new Namespace('chat-back-bottom'); + + const container = useSignal(null); + + const handleScroll = () => { + if (container.value) { + const visibilityHeight = props.visibilityHeight || 200; + const scrollBottom = + container.value.scrollHeight - + container.value.scrollTop - + container.value.offsetHeight; + visible.value = scrollBottom >= visibilityHeight; + } + }; + + const handleClick = () => { + if (container.value) { + container.value.scrollTo({ + top: container.value.scrollHeight, + behavior: 'smooth', + }); + } + }; + + const handleScrollThrottled = throttle(handleScroll, 300); + + useMemo(() => { + buttonStyle.value = { + right: `${props.right}px`, + bottom: `${props.bottom}px`, + }; + }, [props.right, props.bottom]); + + useEffect(() => { + if (props.target) { + const el = document.querySelector(props.target) ?? undefined; + if (el) { + container.value = el; + el.addEventListener('scroll', handleScrollThrottled); + handleScroll(); + } + } + }, []); + + return ( +
+ +
+ ); +}; diff --git a/src/components/chat-container/chat-container.scss b/src/components/chat-container/chat-container.scss index 427fdd0..1b2fe43 100644 --- a/src/components/chat-container/chat-container.scss +++ b/src/components/chat-container/chat-container.scss @@ -92,8 +92,8 @@ $ai-chat: ( align-items: center; justify-content: space-between; height: 60px; - cursor: move; padding: 4px; + cursor: move; border-bottom: 1px solid #{getCssVar('ai-chat', 'border-color')}; } @@ -152,6 +152,7 @@ $ai-chat: ( } @include b(chat-container-content) { + position: relative; flex-grow: 1; overflow: hidden; } diff --git a/src/components/chat-messages/chat-messages.tsx b/src/components/chat-messages/chat-messages.tsx index 5a344a4..367c59c 100644 --- a/src/components/chat-messages/chat-messages.tsx +++ b/src/components/chat-messages/chat-messages.tsx @@ -5,6 +5,7 @@ import { AiChatController } from '../../controller'; import { IChatToolbarItem } from '../../interface'; import { ChatMessageItem } from '../chat-message-item/chat-message-item'; import { ChatToolbar } from '../chat-toolbar/chat-toolbar'; +import { ChatBackBottom } from '../chat-back-bottom/chat-back-bottom'; import './chat-messages.scss'; export interface ChatMessageProps { @@ -59,6 +60,7 @@ export const ChatMessages = (props: ChatMessageProps) => { ); })} + ); }; -- Gitee From ec6390a1ba3a0540ed35e3c3e639b3248b4305bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=94=BF=E6=9D=83?= <1978141412@qq.com> Date: Fri, 7 Mar 2025 18:38:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?style:=20ai=E5=AF=B9=E8=AF=9D=E6=A1=86?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E7=BB=9F=E4=B8=80=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat-input/chat-input.scss | 24 +------------------ src/components/chat-input/chat-input.tsx | 6 +++-- .../markdown-message/markdown-message.scss | 24 ++++++++++++++++--- .../markdown-message/markdown-message.tsx | 3 ++- .../chat-topic-item/chat-topic-item.scss | 5 ++-- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/chat-input/chat-input.scss b/src/components/chat-input/chat-input.scss index 4b5e704..b590e73 100644 --- a/src/components/chat-input/chat-input.scss +++ b/src/components/chat-input/chat-input.scss @@ -14,28 +14,6 @@ border-radius: #{getCssVar('ai-chat', 'border-radius')}; } -@keyframes loading-change { - 0% { - opacity: 1; - } - - 25% { - opacity: 0.5; - } - - 50% { - opacity: 0.1; - } - - 75% { - opacity: 0.5; - } - - 100% { - opacity: 1; - } -} - @include b(chat-input__textarea) { flex-grow: 1; max-height: 6em; @@ -85,8 +63,8 @@ } @include when(loading) { + color: #{getCssVar('ai-chat', 'disabled-color')}; pointer-events: none; - animation: loading-change .8s infinite; } } } diff --git a/src/components/chat-input/chat-input.tsx b/src/components/chat-input/chat-input.tsx index fa4fd48..35251ef 100644 --- a/src/components/chat-input/chat-input.tsx +++ b/src/components/chat-input/chat-input.tsx @@ -18,6 +18,7 @@ import { RecordingSvg, SendSvg, FileSvg, + LoadingSvg, } from '../../icons'; import { AiChatController, AIMaterialFactory } from '../../controller'; import './chat-input.scss'; @@ -128,7 +129,8 @@ export const ChatInput = (props: ChatInputProps) => { }, [input, isLoading]); const onKeyDown = (e: KeyboardEvent) => { - if (e.code === 'Enter') { + // 存在候选词时不请求 + if (e.code === 'Enter' && !e.isComposing) { e.stopPropagation(); if (e.shiftKey === false) { question(); @@ -251,7 +253,7 @@ export const ChatInput = (props: ChatInputProps) => { onClick={question} disabled={isDisableSend} > - + {isLoading.value ? : } 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 ee2ebce..2469fa3 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.scss +++ b/src/components/chat-message-item/markdown-message/markdown-message.scss @@ -10,9 +10,27 @@ padding: 8px; border: 0; - figure > svg { - width: 100%; - min-height: 100px; + figure { + max-width: 400px; + + > svg { + width: 100%; + min-height: 100px; + } + } + + div[data-type="codeBlock"] { + position: relative !important; + display: flex; + padding-top: 46px; + + .cherry-copy-code-block { + position: absolute; + } + + .cherry-edit-code-block { + display: none !important; + } } } 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 e5324aa..0cd63eb 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.tsx +++ b/src/components/chat-message-item/markdown-message/markdown-message.tsx @@ -135,6 +135,7 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { ) : ( ); + thoughtChain.value.title = '思考完成'; } if (thoughtContent) { thoughtChain.value.description = thoughtContent; @@ -154,7 +155,7 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { const { isThoughtCompleted, thoughtContent, answerContent } = parseThinkContent(message.content); thoughtChain.value = { - title: '思考过程', + title: isThoughtCompleted ? '思考完成' : '思考中...', description: thoughtContent || '', icon: isThoughtCompleted ? ( diff --git a/src/components/chat-topic-item/chat-topic-item.scss b/src/components/chat-topic-item/chat-topic-item.scss index 2e1a989..ef4c245 100644 --- a/src/components/chat-topic-item/chat-topic-item.scss +++ b/src/components/chat-topic-item/chat-topic-item.scss @@ -37,10 +37,11 @@ top: 50%; right: 10px; display: flex; + gap: 8px; align-items: center; - justify-content: space-around; - width: 54px; + justify-content: flex-end; height: 24px; + padding: 0 8px; color: #{getCssVar('ai-chat', 'hover-color')}; background-color: #{getCssVar('ai-chat', 'hover-background-color')}; border-radius: 8px; -- Gitee From bcce61acffe3c210834a321df2d05a0f7457dad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=94=BF=E6=9D=83?= <1978141412@qq.com> Date: Fri, 7 Mar 2025 21:05:04 +0800 Subject: [PATCH 3/3] =?UTF-8?q?update:=20=E6=B7=BB=E5=8A=A0=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat-input/chat-input.scss | 5 --- src/components/chat-input/chat-input.tsx | 24 ++++++++++---- .../markdown-message/markdown-message.scss | 9 ++--- src/controller/ai-chat/ai-chat.controller.ts | 33 +++++++++++++++++-- src/entity/chat-message/chat-message.ts | 1 + src/icons/index.ts | 1 + src/icons/stop-circle-svg.tsx | 13 ++++++++ .../i-chat-options/i-chat-options.ts | 6 ++++ 8 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 src/icons/stop-circle-svg.tsx diff --git a/src/components/chat-input/chat-input.scss b/src/components/chat-input/chat-input.scss index b590e73..1e7ceb2 100644 --- a/src/components/chat-input/chat-input.scss +++ b/src/components/chat-input/chat-input.scss @@ -61,11 +61,6 @@ color: #{getCssVar('ai-chat', 'disabled-color')}; pointer-events: none; } - - @include when(loading) { - color: #{getCssVar('ai-chat', 'disabled-color')}; - pointer-events: none; - } } } diff --git a/src/components/chat-input/chat-input.tsx b/src/components/chat-input/chat-input.tsx index 35251ef..5fbd486 100644 --- a/src/components/chat-input/chat-input.tsx +++ b/src/components/chat-input/chat-input.tsx @@ -18,7 +18,7 @@ import { RecordingSvg, SendSvg, FileSvg, - LoadingSvg, + StopCircleSvg, } from '../../icons'; import { AiChatController, AIMaterialFactory } from '../../controller'; import './chat-input.scss'; @@ -169,6 +169,16 @@ export const ChatInput = (props: ChatInputProps) => { setIsPopupOpen(false); }; + const onSendClick = () => { + // 停止生成 + if (isLoading.value) { + props.controller.abortQuestion(); + isLoading.value = false; + } else { + question(); + } + }; + return (
@@ -245,15 +255,15 @@ export const ChatInput = (props: ChatInputProps) => { {recording.value ? : }
- {isLoading.value ? : } + {isLoading.value ? : }
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 2469fa3..eeb840c 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.scss +++ b/src/components/chat-message-item/markdown-message/markdown-message.scss @@ -22,14 +22,11 @@ div[data-type="codeBlock"] { position: relative !important; display: flex; - padding-top: 46px; + padding-top: 24px; - .cherry-copy-code-block { + .cherry-edit-code-block,.cherry-copy-code-block { position: absolute; - } - - .cherry-edit-code-block { - display: none !important; + top: 0; } } } diff --git a/src/controller/ai-chat/ai-chat.controller.ts b/src/controller/ai-chat/ai-chat.controller.ts index 97e8afd..9212c9f 100644 --- a/src/controller/ai-chat/ai-chat.controller.ts +++ b/src/controller/ai-chat/ai-chat.controller.ts @@ -78,6 +78,13 @@ export class AiChatController { */ readonly topicId: string | undefined = undefined; + /** + * @description 当前发送消息标识 + * @type {(string)} + * @memberof AiChatController + */ + public messageId: string = ''; + /** * Creates an instance of AiChatController. * @@ -269,10 +276,11 @@ export class AiChatController { type: 'DEFAULT', content: inputText, }); + this.messageId = createUUID(); await this.opts.question( this.context, this.params, - { appDataEntityId: this.appDataEntityId }, + { appDataEntityId: this.appDataEntityId, messageId: this.messageId }, this.messages.value .filter(item => item.type !== 'ERROR') .map(item => item._origin), @@ -282,6 +290,23 @@ export class AiChatController { } } + /** + * @description 中断请求 + * @memberof AiChatController + */ + abortQuestion(): void { + this.opts.abortQuestion(); + this.addMessage({ + state: 30, + messageid: this.messageId, + role: 'ASSISTANT', + type: 'DEFAULT', + completed: true, + content: '', + }); + this.completeMessage(this.messageId, true); + } + /** * 回填选中的消息 * @@ -329,10 +354,11 @@ export class AiChatController { if (isuser) { this.messages.value.splice(i + 1, this.messages.value.length - i - 1); this.messages.value = [...this.messages.value]; + this.messageId = createUUID(); await this.opts.question( this.context, this.params, - { appDataEntityId: this.appDataEntityId }, + { appDataEntityId: this.appDataEntityId, messageId: this.messageId }, this.messages.value .filter(item => item.type !== 'ERROR') .map(item => item._origin), @@ -340,10 +366,11 @@ export class AiChatController { } else if (i === this.messages.value.length - 1) { this.messages.value.pop(); this.messages.value = [...this.messages.value]; + this.messageId = createUUID(); await this.opts.question( this.context, this.params, - { appDataEntityId: this.appDataEntityId }, + { appDataEntityId: this.appDataEntityId, messageId: this.messageId }, this.messages.value .filter(item => item.type !== 'ERROR') .map(item => item._origin), diff --git a/src/entity/chat-message/chat-message.ts b/src/entity/chat-message/chat-message.ts index 5540c26..ba5c63e 100644 --- a/src/entity/chat-message/chat-message.ts +++ b/src/entity/chat-message/chat-message.ts @@ -52,6 +52,7 @@ export class ChatMessage implements IChatMessage { msg.content = ''; } this.msg.content += msg.content; + this.msg.state = msg.state; } /** diff --git a/src/icons/index.ts b/src/icons/index.ts index 968da63..fea60dd 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -21,3 +21,4 @@ export { PaperclipSvg } from './paperclip-svg'; export { FileSvg } from './upload-svg'; export { MaterialRemoveSvg } from './material-remove-svg'; export { DefaultMaterialSvg } from './default-material-svg'; +export { StopCircleSvg } from './stop-circle-svg'; diff --git a/src/icons/stop-circle-svg.tsx b/src/icons/stop-circle-svg.tsx new file mode 100644 index 0000000..4262bff --- /dev/null +++ b/src/icons/stop-circle-svg.tsx @@ -0,0 +1,13 @@ +// 停止图标 +export const StopCircleSvg = (props: { className?: string }) => ( + + + +); diff --git a/src/interface/i-chat-options/i-chat-options.ts b/src/interface/i-chat-options/i-chat-options.ts index b6d5d09..b6169b1 100644 --- a/src/interface/i-chat-options/i-chat-options.ts +++ b/src/interface/i-chat-options/i-chat-options.ts @@ -117,6 +117,12 @@ export interface IChatOptions extends IChat { question: IChatMessage[], ): Promise; + /** + * @description 中断消息 + * @memberof IChatOptions + */ + abortQuestion(): void; + /** * 聊天窗历史记录获取 * -- Gitee