diff --git a/src/components/back-bottom/back-bottom.scss b/src/components/back-bottom/back-bottom.scss index acfa2abc95c3c3de16cfd8bb7c517868665d4ff4..062d69089a029f679e1c637053d4d5bf38b6548b 100644 --- a/src/components/back-bottom/back-bottom.scss +++ b/src/components/back-bottom/back-bottom.scss @@ -1,5 +1,5 @@ @include b(back-bottom) { - position: fixed; + position: absolute; z-index: 1; display: flex; align-items: center; diff --git a/src/components/back-bottom/back-bottom.tsx b/src/components/back-bottom/back-bottom.tsx index 72e7a20888914e33b279a654bcea4ad34bff3419..61bdac98192138495876ae7e1f7c6be675e83b8a 100644 --- a/src/components/back-bottom/back-bottom.tsx +++ b/src/components/back-bottom/back-bottom.tsx @@ -22,12 +22,12 @@ export const BackBottom = defineComponent({ // 右侧距离 right: { type: [Number, String], - default: '0.5rem', + default: '1rem', }, // 底部距离 bottom: { type: [Number, String], - default: '6.25rem', + default: '1rem', }, // 显示阈值(滚动高度超过该值才显示) offset: { diff --git a/src/components/chart-material/chart-material.scss b/src/components/chart-material/chart-material.scss index e59dd17800e41d9636bbdeba4b08f52d84aacd0e..f04a3ae22c742988a869d1018e8c326cc4bd6bfd 100644 --- a/src/components/chart-material/chart-material.scss +++ b/src/components/chart-material/chart-material.scss @@ -2,5 +2,4 @@ display: flex; flex-wrap: wrap; gap: 0.75rem; - margin-bottom: 0.75rem; } diff --git a/src/components/chart-suggestion/chart-suggestion.scss b/src/components/chart-suggestion/chart-suggestion.scss index 7e021f66f914ab32dbf271ee9dca77c5d56586ce..adcbf519437a9d1da7c613dbab47ef72ed694c5d 100644 --- a/src/components/chart-suggestion/chart-suggestion.scss +++ b/src/components/chart-suggestion/chart-suggestion.scss @@ -1,23 +1,24 @@ @include b('chart-suggestion') { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.3rem; width: 100%; @include e('item') { display: flex; + gap: 0.5rem; align-items: center; align-self: flex-start; justify-content: space-between; height: 2.25rem; padding: 0 1rem; overflow: hidden; - line-height: 2.25rem; + line-height: 1em; text-overflow: ellipsis; white-space: nowrap; - background-color: getCssVar('ai-chat', 'bg-color-1'); + background-color: getCssVar('ai-chat', 'bg-color'); border: 1px solid getCssVar('ai-chat', 'border-color'); - border-radius: 1rem; + border-radius: 0.85rem; transition: 0.3s background-color; @include m('action') { @@ -26,8 +27,8 @@ } svg { - width: 1.25rem; - height: 1.25rem; + width: 1em; + height: 1em; } } } diff --git a/src/components/chart-tool-call/chart-tool-call-item/chart-tool-call-item.scss b/src/components/chart-tool-call/chart-tool-call-item/chart-tool-call-item.scss index dda96eb7db09a8ea2a8856f020aa1145f4b6b9db..9b04817e7c63159bb627d7fbd97fa95e127e9a20 100644 --- a/src/components/chart-tool-call/chart-tool-call-item/chart-tool-call-item.scss +++ b/src/components/chart-tool-call/chart-tool-call-item/chart-tool-call-item.scss @@ -18,7 +18,7 @@ $chart-tool-call-item: ( @include b('chart-tool-call-item') { @include set-component-css-var('chart-tool-call-item', $chart-tool-call-item); - margin-bottom: 0.75rem; + margin-bottom: 0.35rem; background-color: getCssVar(chart-tool-call-item, bg-color); border: 1px solid getCssVar(chart-tool-call-item, border-color); border-radius: 1rem; diff --git a/src/components/chat-container/chat-container.scss b/src/components/chat-container/chat-container.scss index 9de8c418f72562b9f54c33963d9d1776efb48a44..22d9e06e7f832bea6a9b9130dd1570978fcac6ca 100644 --- a/src/components/chat-container/chat-container.scss +++ b/src/components/chat-container/chat-container.scss @@ -6,6 +6,7 @@ $ai-chat: ( 'bg-color-1': #fbf9fa, 'bg-color-2': #fff3, 'bg-color-3': #e9e9e9, + 'mask-bg-color': #000000b3, 'active-bg-color': rgb(219 234 254), // 输入区背景色 'bg-color-4': #f6f3f4, @@ -39,14 +40,9 @@ $ai-chat: ( @include b('chat-container') { @include set-component-css-var('ai-chat', $ai-chat); - position: absolute; + position: fixed; top: 0; left: 0; - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - background: getCssVar('ai-chat', 'bg-color'); // markdown 样式 .cherry { @@ -113,7 +109,36 @@ $ai-chat: ( word-wrap: break-word; white-space: pre-wrap; } - + + @include e('overlay') { + width: 100%; + height: 100%; + background-color: getCssVar('ai-chat', 'mask-bg-color'); + } + + @include e('dialog') { + display: flex; + flex-direction: column; + width: 100%; + background-color: getCssVar('ai-chat', 'bg-color'); + transition: transform 0.3s; + @include m('default') { + height: 100%; + } + @include m('top') { + position: fixed; + top: 0; + left: 0; + height: 80%; + } + @include m('bottom') { + position: fixed; + bottom: 0; + left: 0; + height: 80%; + } + } + @include e('header') { display: flex; flex-shrink: 0; @@ -157,14 +182,34 @@ $ai-chat: ( } } + @include e('icon') { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background-color: getCssVar('ai-chat', 'bg-color-2'); + border-radius: 50%; + + svg { + width: 1.25rem; + height: 1.25rem; + } + } + @include e('content') { + position: relative; flex-grow: 1; min-height: 0; background: getCssVar('ai-chat', 'bg-color-1'); } @include e('footer') { + display: flex; + flex-direction: column; flex-shrink: 0; + gap: 0.75rem; + padding: 0.5rem 1rem; border-top: 1px solid getCssVar('ai-chat', 'border-color'); box-shadow: rgb(0 0 0 / 0%) 0 0 0 0, diff --git a/src/components/chat-container/chat-container.tsx b/src/components/chat-container/chat-container.tsx index 2f419b4cbf793a63ae5212d35dbab368438e0912..8ff9e1d769e24931b4a9471a3895fa50c1a5d693 100644 --- a/src/components/chat-container/chat-container.tsx +++ b/src/components/chat-container/chat-container.tsx @@ -5,9 +5,10 @@ import { IChatToolbarItem, IChatContainerOptions, } from '../../interface'; -import { AIIcon } from '../../icon'; +import { AIIcon, ExitIcon } from '../../icon'; import { ChatMessages, ChatInput } from '../../components'; import { AIChatController, AITopicController } from '../../controller'; +import { ChatInputToolbar } from '../chat-toolbar'; import './chat-container.scss'; export const ChatContainer = defineComponent({ @@ -24,6 +25,10 @@ export const ChatContainer = defineComponent({ type: Object as PropType, required: true, }, + openMode: { + type: String as PropType<'default' | 'top' | 'bottom'>, + default: 'default', + }, caption: { type: String, default: 'AI助手', @@ -77,34 +82,61 @@ export const ChatContainer = defineComponent({ } }); + /** + * @description 点击遮罩 + */ + const onClickOverlay = (): void => { + if (props.openMode === 'default') return; + onClose(); + }; + return { ns, zIndex, onClose, + onClickOverlay, }; }, render() { return ( -
-
-
-
{AIIcon}
-
{this.caption}
+
+
evt.stopPropagation()} + > +
+
+
{AIIcon}
+
{this.caption}
+
+
+
+ {ExitIcon} +
+
+
+
+ +
+
+ +
-
-
-
- -
-
-
); diff --git a/src/components/chat-input/chat-input.scss b/src/components/chat-input/chat-input.scss index 3179070ec4e56191730293505c5e8db8548e176c..099c0e03aeb3116528150eeb24546d9bc29fbb7f 100644 --- a/src/components/chat-input/chat-input.scss +++ b/src/components/chat-input/chat-input.scss @@ -1,6 +1,8 @@ @include b('chat-input') { - padding: 1rem; - + display: flex; + flex-direction: column; + gap: 0.75rem; + @include e('content') { display: flex; gap: 0.75rem; diff --git a/src/components/chat-input/chat-input.tsx b/src/components/chat-input/chat-input.tsx index b8c2dd01fe58f3802ea027fe3923af13a130b6df..8a761d4280b1697691037a3c75b17d77327dac8c 100644 --- a/src/components/chat-input/chat-input.tsx +++ b/src/components/chat-input/chat-input.tsx @@ -8,6 +8,7 @@ import { UploadIcon, OpenVoiceIcon, CloseVoiceIcon, + ImageIcon, } from '../../icon'; import { ChartMaterial } from '../chart-material/chart-material'; import './chat-input.scss'; @@ -25,6 +26,11 @@ export const ChatInput = defineComponent({ setup(props) { const ns = new Namespace('chat-input'); + // 图片列表 + const imageList = ref<{ name: string; url: string; isImage: boolean }[]>( + [], + ); + // 消息 const message = ref(''); @@ -67,11 +73,11 @@ export const ChatInput = defineComponent({ }); /** - * @description 文件上传 + * @description 素材上传 * @param {MouseEvent} event * @returns {*} {Promise} */ - const uploadFile = async (event: MouseEvent): Promise => { + const uploadMaterial = async (event: MouseEvent): Promise => { const materialHelper = AIMaterialFactory.getMaterialHelper( 'ossfile', props.controller, @@ -79,6 +85,41 @@ export const ChatInput = defineComponent({ await materialHelper.excuteAction(event); }; + /** + * @description 图片上传 + * @returns {*} {Promise} + */ + const uploadImage = async (): Promise => { + // 选择文件并上传 + const files = await (window as any).ibiz.util.file.chooseFileAndUpload( + props.controller.context, + props.controller.params, + {}, + { + multiple: true, + accept: 'image/*', + extraParams: { + enableNoAccess: true, + }, + }, + ); + if (!files || !Array.isArray(files)) return; + // 计算下载路径 + const { downloadUrl } = (window as any).ibiz.util.file.calcFileUpDownUrl( + props.controller.context, + props.controller.params, + {}, + { enableNoAccess: true }, + ); + files.forEach(file => { + imageList.value.push({ + isImage: true, + name: file.name, + url: downloadUrl.replace('%fileId%', file.fileid), + }); + }); + }; + /** * @description 发送消息 * @returns {*} {Promise} @@ -88,8 +129,13 @@ export const ChatInput = defineComponent({ if (isLoading.value) { await props.controller.abortQuestion(); } else { - const value = message.value; + const imageStr = imageList.value + .map(img => `![${img.name}](${img.url})`) + .join('\n'); + const value = `${imageStr}\n${message.value}`; + // 重置输入内容 message.value = ''; + imageList.value = []; await props.controller.question(value); } }; @@ -115,8 +161,10 @@ export const ChatInput = defineComponent({ message, isLoading, recording, - uploadFile, + imageList, + uploadImage, onSendMessage, + uploadMaterial, onRemoveMaterial, onSpeechRecognition, }; @@ -129,6 +177,12 @@ export const ChatInput = defineComponent({ materials={this.controller.materials.value} onRemove={this.onRemoveMaterial} /> +
+
+ {ImageIcon} +
{UploadIcon}
diff --git a/src/components/chat-message-toolbar/chat-message-toolbar-item/chat-message-toolbar-item.scss b/src/components/chat-message-toolbar/chat-message-toolbar-item/chat-message-toolbar-item.scss deleted file mode 100644 index 80660488a5c45676d482ea91f452933a5ca0c04c..0000000000000000000000000000000000000000 --- a/src/components/chat-message-toolbar/chat-message-toolbar-item/chat-message-toolbar-item.scss +++ /dev/null @@ -1,14 +0,0 @@ -@include b('chat-message-toolbar-item') { - display: flex; - align-items: center; - - @include e('content') { - display: flex; - align-items: center; - - @include m(icon) { - display: flex; - align-items: center; - } - } -} \ No newline at end of file diff --git a/src/components/chat-message-toolbar/chat-message-toolbar.scss b/src/components/chat-message-toolbar/chat-message-toolbar.scss deleted file mode 100644 index 3babd6c3ba60f6cb4e35aac55790cbe76152c044..0000000000000000000000000000000000000000 --- a/src/components/chat-message-toolbar/chat-message-toolbar.scss +++ /dev/null @@ -1,73 +0,0 @@ -@include b(chat-message-toolbar) { - display: flex; - flex-direction: column; - width: 14.375rem; - overflow: hidden; - font-size: 1rem; - color: getCssVar(ai-chat, color); - background-color: getCssVar(ai-chat, bg-color); - border-radius: 1rem; - - // 头部工具栏(点赞点踩) - @include e(top) { - display: flex; - border-bottom: 1px solid getCssVar(ai-chat, border-color); - - @include m(item) { - @include when(actived) { - background-color: #{getCssVar('ai-chat', 'active-bg-color')}; - } - } - - .#{bem(chat-message-toolbar-item)} { - position: relative; - flex: 1; - justify-content: center; - height: 5.625rem; - - &::after { - position: absolute; - top: 50%; - right: 0; - display: block; - width: 1px; - height: 38%; - content: ''; - border-right: 1px solid getCssVar(ai-chat, border-color); - transform: translateY(-50%); - } - - &:last-child::after { - display: none; - } - } - .#{bem(chat-message-toolbar-item, content)} { - flex-direction: column; - gap: 0.25rem; - } - } - - // 中部工具栏列表 - @include e(center) { - display: flex; - flex-direction: column; - padding: 0.25rem 0; - .#{bem(chat-message-toolbar-item)} { - padding: 0.5rem 1rem; - } - .#{bem(chat-message-toolbar-item, content)} { - gap: 1rem; - height: 2rem; - } - .#{bem(chat-message-toolbar-item, content, icon)} { - min-width: 1rem; - } - } - - .#{bem(chat-message-toolbar-item, content, icon)} { - svg { - width: 1em; - height: 1em; - } - } -} \ No newline at end of file diff --git a/src/components/chat-messages/chat-message-hook.ts b/src/components/chat-messages/chat-message-hook.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ee5258cea6b9db94aab8b539f70745f0439f724 --- /dev/null +++ b/src/components/chat-messages/chat-message-hook.ts @@ -0,0 +1,106 @@ +import { Ref, nextTick, ref } from 'vue'; +import { ChatMessage } from '../../entity'; + +/** + * @description 消息工具栏 + * @param {*} + * @return {*} + */ +export function useMessageToolbar(model: Ref): { + message: Ref; + handleTouchStart: ( + event: MouseEvent | TouchEvent, + message: ChatMessage, + ) => void; + handleTouchEnd: () => void; + closeToolbar: () => void; +} { + const message = ref(); + + let pressTimer; + + /** + * @description 更新位置 + * @param {*} event 事件源 + * @returns {*} + */ + const updatePosition = (event: any): void => { + if (!model.value || !model.value.$el) return; + + // 偏移量 + const offset = 6; + // 获取坐标 + let clientX: number; + let clientY: number; + if (event.touches && event.touches[0]) { + // 触摸设备 + clientX = event.touches[0].clientX; + clientY = event.touches[0].clientY; + } else { + // 鼠标设备 + clientX = event.clientX; + clientY = event.clientY; + } + + let left = clientX + offset; + let top = clientY + offset; + + // 获取模态元素 + const modalWrapper: HTMLElement = + model.value.$el.querySelector('#modal-wrapper'); + if (!modalWrapper) return; + const rect = modalWrapper.getBoundingClientRect(); + const containerWidth = rect.width; + const containerHeight = rect.height; + + // 获取窗口尺寸 + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + // 边界间距 + const margin = 8; + + if (left + containerWidth + margin > windowWidth) { + // 检查右边界 + left = clientX - containerWidth - offset; + } else if (left < margin) { + // 检查左边界 + left = margin; + } + if (top + containerHeight + margin > windowHeight) { + // 检查下边界 + top = clientY - containerHeight - offset; + } else if (top < margin) { + // 检查上边界 + top = margin; + } + + // 更新模态位置 + modalWrapper.style.top = `${top}px`; + modalWrapper.style.left = `${left}px`; + modalWrapper.style.visibility = 'visible'; + }; + + // 触摸开始事件:启动定时器 + const handleTouchStart = ( + event: MouseEvent | TouchEvent, + msg: ChatMessage, + ): void => { + clearTimeout(pressTimer); + pressTimer = setTimeout(() => { + message.value = msg; + nextTick(() => updatePosition(event)); + }, 500); + }; + + // 触摸结束/取消事件:清除定时器 + const handleTouchEnd = (): void => { + clearTimeout(pressTimer); + }; + + const closeToolbar = (): void => { + message.value = undefined; + }; + + return { message, handleTouchStart, handleTouchEnd, closeToolbar }; +} diff --git a/src/components/chat-messages/chat-messages.scss b/src/components/chat-messages/chat-messages.scss index 2d5a754dfdb88e004c8c47e014cd99b4a1f6e107..3982ee815e66d8a45cb9416677b6b7d226d6f2c7 100644 --- a/src/components/chat-messages/chat-messages.scss +++ b/src/components/chat-messages/chat-messages.scss @@ -1,5 +1,4 @@ @include b(chat-messages) { - position: relative; display: flex; flex-direction: column; gap: 0.75rem; diff --git a/src/components/chat-messages/chat-messages.tsx b/src/components/chat-messages/chat-messages.tsx index 32ed3fabc42d646115f0323943b1dec635823bfd..84f837f85ae86bc2a07ea7204a99498088889829 100644 --- a/src/components/chat-messages/chat-messages.tsx +++ b/src/components/chat-messages/chat-messages.tsx @@ -9,7 +9,10 @@ import { Namespace } from '../../utils'; import { AIChatController } from '../../controller'; import { IChatToolbarItem } from '../../interface'; import { BackBottom } from '../back-bottom/back-bottom'; -import { ChatMessageToolbar } from '../chat-message-toolbar/chat-message-toolbar'; +import { ChatMessageToolbar } from '../chat-toolbar'; +import { CustomModal } from '../custom-modal/custom-modal'; +import { useMessageToolbar } from './chat-message-hook'; +import { ChatMessage } from '../../entity'; import './chat-messages.scss'; export const ChatMessages = defineComponent({ @@ -31,6 +34,11 @@ export const ChatMessages = defineComponent({ const containerRef = ref(); + const modalRef = ref(); + + const { message, handleTouchStart, handleTouchEnd, closeToolbar } = + useMessageToolbar(modalRef); + // 加载更多条数 const batchSize: number = 5; // 当前显示的消息数量 @@ -92,23 +100,18 @@ export const ChatMessages = defineComponent({ // 检查是否滚动到顶部附近(触发加载更多) if (scrollTop < 100 && !isLoading.value && hasMoreMessages.value) { isLoading.value = true; - const oldScrollHeight = container.scrollHeight; // 加载前的滚动容器总高度 - const oldScrollTop = container.scrollTop; // 加载前的滚动位置 - // 加载更多消息 setTimeout(() => { - const newCount = Math.min( + displayCount.value = Math.min( displayCount.value + batchSize, messages.value.length, ); - displayCount.value = newCount; - + const oldScrollHeight = container.scrollHeight; // 保持当前滚动位置(加载更多后不跳转) setTimeout(() => { - const newScrollHeight = container.scrollHeight; // 加载后的滚动容器总高度 - // 核心计算:新的滚动位置 = 新增高度 + 原来的滚动位置 + const newScrollHeight = container.scrollHeight; container.scrollTop = - newScrollHeight - oldScrollHeight + oldScrollTop; + newScrollHeight - oldScrollHeight + container.scrollTop; isLoading.value = false; }, 0); }, 300); // 添加一点延迟让用户体验更好 @@ -119,57 +122,67 @@ export const ChatMessages = defineComponent({ isAutoScroll.value = isNearBottom; }; + /** + * @description 绘制消息 + * @param {ChatMessage} msg + */ + const renderMessage = (msg: ChatMessage) => { + switch (msg.type) { + case 'DEFAULT': + return msg.role === 'ASSISTANT' ? ( + + ) : ( + + ); + case 'ERROR': + return ; + default: + return ; + } + }; + return { ns, + message, messages, + modalRef, containerRef, - handleScroll, visibleMessages, + handleScroll, + closeToolbar, + renderMessage, + handleTouchStart, + handleTouchEnd, }; }, render() { return (
- {this.messages.map(message => { - const toolbar = ( + {this.visibleMessages.map(message => { + return ( +
this.handleTouchStart(evt, message)} + onTouchstart={evt => this.handleTouchStart(evt, message)} + > + {this.renderMessage(message)} +
+ ); + })} + + {this.message && ( + - ); - - switch (message.type) { - case 'DEFAULT': - return message.role === 'ASSISTANT' ? ( - - {{ modalToolbar: () => toolbar }} - - ) : ( - - {{ modalToolbar: () => toolbar }} - - ); - case 'ERROR': - return ( - - ); - default: - return ( - - ); - } - })} - + + )}
); }, diff --git a/src/components/chat-messages/common/error-message/error-message.tsx b/src/components/chat-messages/common/error-message/error-message.tsx index 233528e1bafda7a60510b1aa719cf82f7da482e2..219cbcf1a78bfab18e971b42fb16b30ca4191bf7 100644 --- a/src/components/chat-messages/common/error-message/error-message.tsx +++ b/src/components/chat-messages/common/error-message/error-message.tsx @@ -26,7 +26,7 @@ export const ErrorMessage = defineComponent({
- {this.$slots.default?.()} + {this.$slots.header?.()}

{this.message.content}

diff --git a/src/components/chat-messages/common/markdown-message/markdown-message.scss b/src/components/chat-messages/common/markdown-message/markdown-message.scss index 7c17104989e09fd16946df2b6786520bc2c62495..52290d6e633aed3137b5615be133347627afe0c1 100644 --- a/src/components/chat-messages/common/markdown-message/markdown-message.scss +++ b/src/components/chat-messages/common/markdown-message/markdown-message.scss @@ -20,10 +20,12 @@ @include e('right') { display: flex; + gap: 0.35rem; flex-direction: column; max-width: calc(100% - 5.5rem); @include m('content') { display: flex; + gap: 0.75rem; flex-direction: column; flex-grow: 1; padding: 0.5rem 1rem; @@ -32,14 +34,14 @@ border-bottom-left-radius: 0.375rem; box-shadow: getCssVar('ai-chat', 'box-shadow'); } - @include e('tool-call') { - flex-shrink: 0; - } - @include e('thought') { - flex-shrink: 0; - } - @include e('message') { - flex-grow: 1; - } + } + @include e('tool-call') { + flex-shrink: 0; + } + @include e('thought') { + flex-shrink: 0; + } + @include e('message') { + flex-grow: 1; } } diff --git a/src/components/chat-messages/common/markdown-message/markdown-message.tsx b/src/components/chat-messages/common/markdown-message/markdown-message.tsx index 0ed611adf188ee3e0ffde9b807f2061c0d94a1a7..436c84dd999fd1288ea8ac9599797c4ff119f159 100644 --- a/src/components/chat-messages/common/markdown-message/markdown-message.tsx +++ b/src/components/chat-messages/common/markdown-message/markdown-message.tsx @@ -8,8 +8,6 @@ import { AIChatController } from '../../../../controller'; import { ChatThoughtChain } from '../../../chart-thought-chain/chart-thought-chain'; import { ChartToolCall } from '../../../chart-tool-call/chart-tool-call'; import { ChartSuggestion } from '../../../chart-suggestion/chart-suggestion'; -import { useMessageToolbarRender } from '../use-message'; -import { CustomModal } from '../../../custom-modal/custom-modal'; import './markdown-message.scss'; export const MarkdownMessage = defineComponent({ @@ -29,9 +27,6 @@ export const MarkdownMessage = defineComponent({ const cherryEditor = ref(null); - const { isToolbarVisible, handleTouchStart, handleTouchEnd, closeToolbar } = - useMessageToolbarRender(); - /** * @description 解析内容 * @param {string} [text] @@ -101,25 +96,15 @@ export const MarkdownMessage = defineComponent({ return { ns, UUID, - isToolbarVisible, - handleTouchStart, - handleTouchEnd, - closeToolbar, }; }, render() { return ( -
+
{AIIcon}
- {this.$slots.default?.()} + {this.$slots.header?.()}
{this.message.toolcalls && ( @@ -149,12 +134,6 @@ export const MarkdownMessage = defineComponent({ )}
- - {this.$slots.modalToolbar?.()} -
); }, diff --git a/src/components/chat-messages/common/unknown-message/unknown-message.tsx b/src/components/chat-messages/common/unknown-message/unknown-message.tsx index 2b03455bc88a18747c23d4f6ab96e328cdd57f45..1560f8aface544028ac30132fa9a7571fe0eccc3 100644 --- a/src/components/chat-messages/common/unknown-message/unknown-message.tsx +++ b/src/components/chat-messages/common/unknown-message/unknown-message.tsx @@ -23,8 +23,13 @@ export const UnknownMessage = defineComponent({
{AIIcon}
+
+ {this.$slots.header?.()} +
-

暂未支持的消息类型: {this.message.type}

+

+ 暂未支持的消息类型: {this.message.type} +

diff --git a/src/components/chat-messages/common/use-message.ts b/src/components/chat-messages/common/use-message.ts deleted file mode 100644 index 8ac2898012669d9a55edef9dd3715a93c9121404..0000000000000000000000000000000000000000 --- a/src/components/chat-messages/common/use-message.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Ref, ref } from 'vue'; - -/** - * @description 消息工具栏绘制工具 - * @param {*} - * @return {*} - */ -export function useMessageToolbarRender(): { - isToolbarVisible: Ref; - handleTouchStart: () => void; - handleTouchEnd: () => void; - closeToolbar: () => void; -} { - const isToolbarVisible = ref(false); - - let pressTimer; - - // 触摸开始事件:启动定时器 - const handleTouchStart = (): void => { - clearTimeout(pressTimer); - pressTimer = setTimeout(() => { - isToolbarVisible.value = true; - }, 500); - }; - - // 触摸结束/取消事件:清除定时器 - const handleTouchEnd = (): void => { - clearTimeout(pressTimer); - }; - - const closeToolbar = (): void => { - isToolbarVisible.value = false; - }; - - return { isToolbarVisible, handleTouchStart, handleTouchEnd, closeToolbar }; -} diff --git a/src/components/chat-messages/common/user-message/user-message.scss b/src/components/chat-messages/common/user-message/user-message.scss index 986bd02c2e463e5c5baf5d83b3f999df374fc22f..b6a53aa2765ed18cd91504a0da12209dcb559802 100644 --- a/src/components/chat-messages/common/user-message/user-message.scss +++ b/src/components/chat-messages/common/user-message/user-message.scss @@ -25,6 +25,9 @@ flex-direction: column; max-width: calc(100% - 5.5rem); @include m('content') { + display: flex; + flex-direction: column; + flex-grow: 1; padding: 0.5rem 1rem; background-color: getCssVar('ai-chat', 'light-primary-color'); border-radius: 1rem; @@ -33,7 +36,13 @@ } } + @include e('material') { + flex-shrink: 0; + margin-bottom: 0.75rem; + } + @include e('message') { + flex-grow: 1; // markdown 样式 > .cherry { .cherry-markdown pre { diff --git a/src/components/chat-messages/common/user-message/user-message.tsx b/src/components/chat-messages/common/user-message/user-message.tsx index 561bc66b2dd350ad3ef5da1397fa9756b46d5482..9941afb1b4cae5158d95f4db1820e748c79b319c 100644 --- a/src/components/chat-messages/common/user-message/user-message.tsx +++ b/src/components/chat-messages/common/user-message/user-message.tsx @@ -6,8 +6,6 @@ import { AIChatController } from '../../../../controller'; import { IChatMessage } from '../../../../interface'; import { USERIcon } from '../../../../icon'; import { ChartMaterial } from '../../../chart-material/chart-material'; -import { useMessageToolbarRender } from '../use-message'; -import { CustomModal } from '../../../custom-modal/custom-modal'; import './user-message.scss'; export const UserMessage = defineComponent({ @@ -27,9 +25,6 @@ export const UserMessage = defineComponent({ const cherryEditor = ref(null); - const { isToolbarVisible, handleTouchStart, handleTouchEnd, closeToolbar } = - useMessageToolbarRender(); - const material = MaterialResourceParser.parseMixedContent( props.message.content, ); @@ -63,25 +58,15 @@ export const UserMessage = defineComponent({ ns, UUID, material, - isToolbarVisible, - handleTouchStart, - handleTouchEnd, - closeToolbar, }; }, render() { return ( -
+
{USERIcon}
- {this.$slots.default?.()} + {this.$slots.header?.()}
{this.material.hasResources && ( @@ -94,12 +79,6 @@ export const UserMessage = defineComponent({
- - {this.$slots.modalToolbar?.()} -
); }, diff --git a/src/components/chat-toolbar/chat-input-toolbar/chat-input-toolbar.scss b/src/components/chat-toolbar/chat-input-toolbar/chat-input-toolbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..10bbc8baff83bc1c29d142a41a8881eedd3f0341 --- /dev/null +++ b/src/components/chat-toolbar/chat-input-toolbar/chat-input-toolbar.scss @@ -0,0 +1,37 @@ +@include b('chat-input-toolbar') { + font-size: 0.875rem; + @include e(wrapper) { + display: flex; + gap: 0.5rem; + align-items: center; + width: 100%; + overflow-x: auto; + + // 隐藏滚动条 + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } + } + + .#{bem(chat-toolbar-item)} { + display: flex; + align-items: center; + padding: 0.5rem; + white-space: nowrap; + border: 1px solid getCssVar(ai-chat, border-color); + border-radius: 0.5rem; + } + + .#{bem(chat-toolbar-item, content)} { + display: flex; + gap: 0.25rem; + align-items: center; + } + + svg { + width: 1rem; + height: 1rem; + } +} \ No newline at end of file diff --git a/src/components/chat-toolbar/chat-input-toolbar/chat-input-toolbar.tsx b/src/components/chat-toolbar/chat-input-toolbar/chat-input-toolbar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..106aaee90620354ec1ea89bec85d30a81f2bfa4a --- /dev/null +++ b/src/components/chat-toolbar/chat-input-toolbar/chat-input-toolbar.tsx @@ -0,0 +1,85 @@ +import { defineComponent, PropType, VNode } from 'vue'; +import { Namespace } from '../../../utils'; +import { IChatToolbarItem, ITopic } from '../../../interface'; +import { ClearDialogueIcon, ResetDialogueIcon } from '../../../icon'; +import { AIChatController } from '../../../controller'; +import { useToolbar } from '../chat-toolbar-hook'; +import { ChatToolbarItem } from '../chat-toolbar-item/chat-toolbar-item'; +import './chat-input-toolbar.scss'; + +export const ChatInputToolbar = defineComponent({ + props: { + controller: { + type: Object as PropType, + required: true, + }, + data: { + type: Object as PropType, + }, + items: { + type: Array as PropType, + default: () => [], + }, + }, + setup(props) { + const ns = new Namespace('chat-input-toolbar'); + const { handleItemClick } = useToolbar(props); + + /** + * 工具栏 + */ + const toolbarItems: IChatToolbarItem[] = [ + { + label: '重置对话', + title: '重置对话', + icon: (): VNode => { + return ResetDialogueIcon; + }, + onClick: (): void => { + props.controller.resetTopic(); + }, + }, + { + label: '清空对话', + title: '清空对话', + icon: (): VNode => { + return ClearDialogueIcon; + }, + onClick: (): void => { + props.controller.clearTopic(); + }, + }, + ]; + + // 初始化模型 + const initModel = (): void => { + toolbarItems.push(...props.items); + }; + + initModel(); + + return { + ns, + toolbarItems, + handleItemClick, + }; + }, + render() { + return ( +
+
+ {this.toolbarItems.map(_item => { + return ( + + ); + })} +
+
+ ); + }, +}); diff --git a/src/components/chat-toolbar/chat-message-toolbar/chat-message-toolbar.scss b/src/components/chat-toolbar/chat-message-toolbar/chat-message-toolbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..2a52393754a8ff2730e93fdf3b3b7fc5453bc601 --- /dev/null +++ b/src/components/chat-toolbar/chat-message-toolbar/chat-message-toolbar.scss @@ -0,0 +1,73 @@ +@include b(chat-message-toolbar) { + display: flex; + flex-direction: column; + width: 10rem; + overflow: hidden; + font-size: 1rem; + color: getCssVar(ai-chat, color); + background-color: getCssVar(ai-chat, bg-color); + border-radius: 1rem; + + // 头部工具栏(点赞点踩) + @include e(top) { + display: flex; + border-bottom: 1px solid getCssVar(ai-chat, border-color); + + @include m(item) { + @include when(actived) { + background-color: #{getCssVar('ai-chat', 'active-bg-color')}; + } + } + + .#{bem(chat-toolbar-item)} { + position: relative; + flex: 1; + justify-content: center; + height: 5.625rem; + + &::after { + position: absolute; + top: 50%; + right: 0; + display: block; + width: 1px; + height: 38%; + content: ''; + border-right: 1px solid getCssVar(ai-chat, border-color); + transform: translateY(-50%); + } + + &:last-child::after { + display: none; + } + } + .#{bem(chat-toolbar-item, content)} { + flex-direction: column; + gap: 0.25rem; + } + } + + // 中部工具栏列表 + @include e(center) { + display: flex; + flex-direction: column; + padding: 0.25rem 0; + .#{bem(chat-toolbar-item)} { + padding: 0.5rem 1rem; + } + .#{bem(chat-toolbar-item, content)} { + gap: 1rem; + height: 2rem; + } + .#{bem(chat-toolbar-item, content, icon)} { + min-width: 1rem; + } + } + + .#{bem(chat-toolbar-item, content, icon)} { + svg { + width: 1em; + height: 1em; + } + } +} diff --git a/src/components/chat-message-toolbar/chat-message-toolbar.tsx b/src/components/chat-toolbar/chat-message-toolbar/chat-message-toolbar.tsx similarity index 75% rename from src/components/chat-message-toolbar/chat-message-toolbar.tsx rename to src/components/chat-toolbar/chat-message-toolbar/chat-message-toolbar.tsx index f07ca3a89fbfc0411d1205f1cf6a14755be7a340..35745574686097866a45a184b8006acf7a5ae921 100644 --- a/src/components/chat-message-toolbar/chat-message-toolbar.tsx +++ b/src/components/chat-toolbar/chat-message-toolbar/chat-message-toolbar.tsx @@ -1,23 +1,23 @@ import { computed, defineComponent, PropType, ref, VNode } from 'vue'; -import { Namespace } from '../../utils'; -import { IChatMessage, IChatToolbarItem } from '../../interface'; +import { Namespace } from '../../../utils'; +import { IChatMessage, IChatToolbarItem } from '../../../interface'; import { CopyIcon, - RemoveIcon, - DislikeIcon, FillIcon, LikeIcon, + RemoveIcon, + DislikeIcon, RefreshIcon, -} from '../../icon'; -import { AIChatController } from '../../controller'; -import { ChatMessageToolbarItem } from './chat-message-toolbar-item/chat-message-toolbar-item'; -import { ChatMessage } from '../../entity'; +} from '../../../icon'; +import { AIChatController } from '../../../controller'; +import { useToolbar } from '../chat-toolbar-hook'; +import { ChatToolbarItem } from '../chat-toolbar-item/chat-toolbar-item'; import './chat-message-toolbar.scss'; export const ChatMessageToolbar = defineComponent({ props: { controller: { - type: AIChatController, + type: Object as PropType, required: true, }, data: { @@ -35,6 +35,7 @@ export const ChatMessageToolbar = defineComponent({ }, setup(props) { const ns = new Namespace('chat-message-toolbar'); + const { handleItemClick } = useToolbar(props); /** * 头部工具栏(点赞点踩) @@ -193,46 +194,6 @@ export const ChatMessageToolbar = defineComponent({ return props.data?.state === 20 && props.data?.completed !== true; }); - /** - * 处理项点击 - */ - const handleItemClick = (e: MouseEvent, item: IChatToolbarItem): void => { - const tempData = { - ...props.data, - }; - // 特殊处理消息数据 - if (props.data instanceof ChatMessage) { - Object.assign(tempData, { topic: props.controller.topic }); - // 计算真实文本 - tempData.msg.realcontent = props.data.realcontent; - } else { - if (!tempData.data) tempData.data = {}; - Object.assign(tempData.data!, { - messages: props.controller.messages.value, - }); - } - if (item.onClick && typeof item.onClick === 'function') { - item.onClick( - e, - item, - props.controller.context, - props.controller.params, - tempData, - ); - } else { - const { extendToolbarClick } = props.controller.opts; - if (extendToolbarClick && typeof extendToolbarClick === 'function') { - extendToolbarClick( - e, - item, - props.controller.context, - props.controller.params, - tempData, - ); - } - } - }; - return { ns, isLoadding, @@ -243,7 +204,7 @@ export const ChatMessageToolbar = defineComponent({ }, render() { return ( -
+
{this.topItems.length ? (
{this.topItems.map(item => { @@ -251,11 +212,14 @@ export const ChatMessageToolbar = defineComponent({ (item.id === 'islike' && this.data.islike === '1') || (item.id === 'isdislike' && this.data.isdislike === '1'); return ( - ); @@ -266,7 +230,7 @@ export const ChatMessageToolbar = defineComponent({
{this.centerItems.map(item => { return ( - ) : null} - {/*
TODO更多
*/}
); }, diff --git a/src/components/chat-toolbar/chat-toolbar-hook.ts b/src/components/chat-toolbar/chat-toolbar-hook.ts new file mode 100644 index 0000000000000000000000000000000000000000..610762ab76373de01545bfe03e598610a3584b1b --- /dev/null +++ b/src/components/chat-toolbar/chat-toolbar-hook.ts @@ -0,0 +1,57 @@ +import { IChatMessage, IChatToolbarItem, ITopic } from '../../interface'; +import { ChatMessage } from '../../entity'; +import { AIChatController } from '../../controller'; + +/** + * @description 工具栏 + * @param {*} + * @return {*} + */ +export function useToolbar(props: { + controller: AIChatController; + data?: ITopic | IChatMessage; +}): { + handleItemClick: (_e: MouseEvent, _item: IChatToolbarItem) => void; +} { + /** + * 处理项点击 + */ + const handleItemClick = (e: MouseEvent, item: IChatToolbarItem): void => { + const tempData: any = { + ...props.data, + }; + // 特殊处理消息数据 + if (props.data instanceof ChatMessage) { + Object.assign(tempData, { topic: props.controller.topic }); + // 计算真实文本 + tempData.msg.realcontent = props.data.realcontent; + } else { + if (!tempData.data) tempData.data = {}; + Object.assign(tempData.data!, { + messages: props.controller.messages.value, + }); + } + if (item.onClick && typeof item.onClick === 'function') { + item.onClick( + e, + item, + props.controller.context, + props.controller.params, + tempData, + ); + } else { + const { extendToolbarClick } = props.controller.opts; + if (extendToolbarClick && typeof extendToolbarClick === 'function') { + extendToolbarClick( + e, + item, + props.controller.context, + props.controller.params, + tempData, + ); + } + } + }; + + return { handleItemClick }; +} diff --git a/src/components/chat-toolbar/chat-toolbar-item/chat-toolbar-item.scss b/src/components/chat-toolbar/chat-toolbar-item/chat-toolbar-item.scss new file mode 100644 index 0000000000000000000000000000000000000000..b9b7f3eb9c61d0b417d892dfd1800ca65f382397 --- /dev/null +++ b/src/components/chat-toolbar/chat-toolbar-item/chat-toolbar-item.scss @@ -0,0 +1,23 @@ +@include b('chat-toolbar-item') { + display: flex; + align-items: center; + + @include e('content') { + display: flex; + align-items: center; + line-height: 1em; + + @include m(icon) { + display: flex; + align-items: center; + line-height: 1em; + } + } + + @include when(disabled) { + cursor: not-allowed; + .#{bem(chat-toolbar-item)} { + background-color: #{getCssVar('ai-chat', 'disabled-color')}; + } + } +} diff --git a/src/components/chat-message-toolbar/chat-message-toolbar-item/chat-message-toolbar-item.tsx b/src/components/chat-toolbar/chat-toolbar-item/chat-toolbar-item.tsx similarity index 85% rename from src/components/chat-message-toolbar/chat-message-toolbar-item/chat-message-toolbar-item.tsx rename to src/components/chat-toolbar/chat-toolbar-item/chat-toolbar-item.tsx index 3363b1e8e9d41da48cd95a305272cad39faf0e28..61908aca67a49d895a99489187e4033ded96abcf 100644 --- a/src/components/chat-message-toolbar/chat-message-toolbar-item/chat-message-toolbar-item.tsx +++ b/src/components/chat-toolbar/chat-toolbar-item/chat-toolbar-item.tsx @@ -1,9 +1,9 @@ import { defineComponent, PropType, VNode } from 'vue'; import { isSvg, Namespace } from '../../../utils'; import { IChatToolbarItem } from '../../../interface'; -import './chat-message-toolbar-item.scss'; +import './chat-toolbar-item.scss'; -export const ChatMessageToolbarItem = defineComponent({ +export const ChatToolbarItem = defineComponent({ props: { model: { type: Object as PropType, @@ -15,16 +15,14 @@ export const ChatMessageToolbarItem = defineComponent({ }, disabled: { type: Boolean, - }, - className: { - type: String, + default: false, }, }, emits: { click: (_e: MouseEvent, _item: IChatToolbarItem) => true, }, setup(props, { emit }) { - const ns = new Namespace('chat-message-toolbar-item'); + const ns = new Namespace('chat-toolbar-item'); /** * 是否隐藏 @@ -92,12 +90,11 @@ export const ChatMessageToolbarItem = defineComponent({ return (
true, + close: () => true, }, setup(_props, { emit }) { const ns = new Namespace('custom-modal'); + const handleMaskClick = (): void => { - emit('close', false); + emit('close'); }; return { @@ -24,13 +19,12 @@ export const CustomModal = defineComponent({ }; }, render() { - if (!this.visible) { - return null; - } return (
-
{this.$slots.default?.()}
+
); }, diff --git a/src/controller/ai-chat/ai-chat.controller.ts b/src/controller/ai-chat/ai-chat.controller.ts index fab66426400cdc2d819ca58c037eec772dcdb2f9..e2eb087cbd17150ef6dd9375a308a93d820fa73b 100644 --- a/src/controller/ai-chat/ai-chat.controller.ts +++ b/src/controller/ai-chat/ai-chat.controller.ts @@ -381,7 +381,10 @@ export class AIChatController { const data = { id: this.getHistoryStoreKey(this.topicId), data: this.messages.value.map(item => { - return { ...item._origin, toolcalls: item.toolcalls ? [...item.toolcalls] : [] }; + return { + ...item._origin, + toolcalls: item.toolcalls ? [...item.toolcalls] : [], + }; }), timestamp: new Date().getTime(), }; @@ -1076,6 +1079,7 @@ export class AIChatController { * @memberof AiChatController */ async destroyed(): Promise { + await this.abortQuestion(); this.evt.reset(); } } diff --git a/src/controller/chat/chat.controller.ts b/src/controller/chat/chat.controller.ts index 4426aa8bc915864fc623aceac794547680caa992..a110743de65edc893c3933417a2460fc97eeec6a 100644 --- a/src/controller/chat/chat.controller.ts +++ b/src/controller/chat/chat.controller.ts @@ -136,7 +136,7 @@ export class ChatController { if (resourceMode === 'LOCAL') await this.initIndexDB(); this.backupChatOptions = opts; - this.close(); + await this.close(); this.container = document.createElement('div'); this.container.classList.add('ibiz-ai-chat'); @@ -200,8 +200,9 @@ export class ChatController { contentToolbarItems: chatOptions.contentToolbarItems, footerToolbarItems: chatOptions.footerToolbarItems, questionToolbarItems: chatOptions.questionToolbarItems, - close: () => { - this.close(); + openMode: opts.containerOptions?.openMode, + onClose: async () => { + await this.close(); if (chatOptions.closed) { chatOptions.closed( chatOptions.context, @@ -373,12 +374,13 @@ export class ChatController { : opts.caption, enableBackFill: this.backupChatOptions?.containerOptions?.enableBackFill, - autoClose: this.backupChatOptions?.containerOptions?.autoClose, + autoClose: this.backupChatOptions?.containerOptions?.autoClose, contentToolbarItems: opts.contentToolbarItems, footerToolbarItems: opts.footerToolbarItems, questionToolbarItems: opts.questionToolbarItems, - close: () => { - this.close(); + openMode: this.backupChatOptions?.containerOptions?.openMode, + onClose: async () => { + await this.close(); if (opts.closed) opts.closed(opts.context, opts.params, aiChat.getAllMessages()); }, @@ -418,22 +420,20 @@ export class ChatController { * @author chitanda * @date 2023-10-13 17:10:10 */ - close(): void { + async close(): Promise { + await Promise.all( + Array.from(this.aiTopicMap).map(aiChat => aiChat[1].destroyed()), + ); // 卸载Vue应用 if (this.App && this.container) { this.App.unmount(); this.App = null; } - // 移除DOM元素 if (this.container && document.body.contains(this.container)) { document.body.removeChild(this.container); this.container = undefined; } - - this.aiTopicMap.forEach(aiChat => { - aiChat.destroyed(); - }); } } diff --git a/src/entity/chat-message/chat-message.ts b/src/entity/chat-message/chat-message.ts index d98e3e598bab29e43f2919b8e5676f6b1cd2f45c..e1d48d116a194b322ea616930185f5c18745387e 100644 --- a/src/entity/chat-message/chat-message.ts +++ b/src/entity/chat-message/chat-message.ts @@ -105,7 +105,7 @@ export class ChatMessage implements IChatMessage { return this.msg.realmessageid; } - constructor(public msg: IChatMessage) { + constructor(protected msg: IChatMessage) { this.toolcalls = msg.toolcalls || []; this.allcontent = msg.content; } diff --git a/src/icon/svg.tsx b/src/icon/svg.tsx index f54c98f293b1dc69bb49807c39f0868490216ff9..098be1b65fff368ee4937b60958d235e5222ec94 100644 --- a/src/icon/svg.tsx +++ b/src/icon/svg.tsx @@ -303,16 +303,12 @@ export const UploadIcon = ( fill='currentColor' > - ); @@ -360,7 +356,6 @@ export const RefreshIcon = ( ); - /** * 点赞 */ @@ -453,3 +448,45 @@ export const NewDialogueIcon = ( > ); +/** + * 图片 + */ +export const ImageIcon = ( + + + + +); +/** + * 退出 + */ +export const ExitIcon = ( + + + +); diff --git a/src/interface/i-chat-container/i-chat-container.ts b/src/interface/i-chat-container/i-chat-container.ts index 1c89dc783397377edde7b7d3eecf91006b811bcf..f5536455370e78e02c6e338c2e1496d0d002bac9 100644 --- a/src/interface/i-chat-container/i-chat-container.ts +++ b/src/interface/i-chat-container/i-chat-container.ts @@ -103,4 +103,11 @@ export interface IChatContainerOptions { * @memberof IChatContainerOptions */ autoClose?: IAutoClose; + + /** + * @description AI窗口的打开模式 + * @type {('default' | 'top' | 'bottom')} + * @memberof IChatContainerOptions + */ + openMode?: 'default' | 'top' | 'bottom'; } diff --git a/src/interface/i-chat-message/i-chat-message.ts b/src/interface/i-chat-message/i-chat-message.ts index 8367773da29fc205b55c580e5caeaf1f5f7c69d7..ea9b14d0239b8595e0984ca4bd4f71f3464ec261 100644 --- a/src/interface/i-chat-message/i-chat-message.ts +++ b/src/interface/i-chat-message/i-chat-message.ts @@ -11,12 +11,6 @@ import { IPortalAsyncAction } from '../i-portal-async-action/i-portal-async-acti * @interface IChatMessage */ export interface IChatMessage { - /** - * AI聊天消息 - * - * @memberof IChatMessage - */ - msg: IChatMessage; /** * 消息标识