From abce898078ed7af30f20858f82c3948c5a2d6064 Mon Sep 17 00:00:00 2001 From: jianglinjun Date: Tue, 7 May 2024 18:44:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0AI=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=88=B7=E6=96=B0=EF=BC=8C=E5=A4=8D=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat-container/chat-container.tsx | 82 ++++++++++++++++++- .../markdown-message/markdown-message.scss | 33 ++++---- .../markdown-message/markdown-message.tsx | 52 +++++++++--- src/controller/ai-chat/ai-chat.controller.ts | 42 ++++++++++ src/icons/close-full-screen-svg.tsx | 6 ++ src/icons/close-svg.tsx | 2 +- src/icons/copy-svg.tsx | 6 ++ src/icons/delete-svg.tsx | 6 ++ src/icons/full-screen-svg.tsx | 5 ++ src/icons/index.ts | 7 +- src/icons/refresh-svg.tsx | 6 ++ src/utils/util/util.ts | 32 ++++++++ 12 files changed, 245 insertions(+), 34 deletions(-) create mode 100644 src/icons/close-full-screen-svg.tsx create mode 100644 src/icons/copy-svg.tsx create mode 100644 src/icons/delete-svg.tsx create mode 100644 src/icons/full-screen-svg.tsx create mode 100644 src/icons/refresh-svg.tsx diff --git a/src/components/chat-container/chat-container.tsx b/src/components/chat-container/chat-container.tsx index 0837f60..1d9047e 100644 --- a/src/components/chat-container/chat-container.tsx +++ b/src/components/chat-container/chat-container.tsx @@ -4,7 +4,7 @@ import { Namespace } from '../../utils'; import { ChatMessages } from '../chat-messages/chat-messages'; import { ChatInput } from '../chat-input/chat-input'; import { AiChatController } from '../../controller'; -import { CloseSgv } from '../../icons'; +import { CloseSvg, FullScreenSvg, CloseFullScreenSvg } from '../../icons'; import './chat-container.scss'; import { AIChatConst } from '../../constants'; @@ -26,6 +26,16 @@ export interface ChatContainerProps { close: () => void; } +interface ChatContainerState { + /** + * 全屏状态 + * + * @author ljx + * @date 2024-05-07 15:10:31 + */ + isFullScreen: boolean; +} + /** * 聊天窗口容器,可拖拽,可缩放 * @@ -35,7 +45,18 @@ export interface ChatContainerProps { * @class ChatContainer * @extends {Component} */ -export class ChatContainer extends Component { +export class ChatContainer extends Component< + ChatContainerProps, + ChatContainerState +> { + constructor(props: ChatContainerProps | undefined) { + super(props); + // 初始化状态 + this.state = { + isFullScreen: false, + }; + } + ns = new Namespace('chat-container'); containerRef = createRef(); @@ -145,12 +166,65 @@ export class ChatContainer extends Component { this.props.close(); } + /** + * 全屏 + * + * @author ljx + * @date 2024-05-07 15:10:31 + */ + fullScreen(): void { + const container = this.containerRef.current; + if (container) { + container.requestFullscreen(); + this.setState({ isFullScreen: true }); + } + } + + /** + * 关闭全屏 + * + * @author ljx + * @date 2024-05-07 15:10:31 + */ + closeFullScreen(): void { + if (this.state.isFullScreen) { + document?.exitFullscreen(); + this.setState({ isFullScreen: false }); + } + } + render() { return ( -
+
AIChat
+ {this.state.isFullScreen ? ( +
+ +
+ ) : ( +
+ +
+ )}
{ )} ${this.ns.be('header-action-wrapper', 'action-close')}`} onClick={this.close.bind(this)} > - +
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 f73daa8..80b1b97 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.scss +++ b/src/components/chat-message-item/markdown-message/markdown-message.scss @@ -18,38 +18,30 @@ @include b(markdown-message-header) { display: flex; + margin-bottom: 8px; @include e(caption) { - flex-grow: 1; + width: auto; + min-width: 32px; + min-height: 32px; padding: 6px; + margin-right: 2px; font-size: 14px; font-weight: 800; color: #{getCssVar('ai-chat', 'color')}; } -} - -@include b(markdown-message-footer) { - display: flex; - justify-content: flex-end; -} - -@include b(markdown-message-footer-wrapper) { - display: flex; - gap: 6px; - align-items: center; - padding: 6px; - color: #{getCssVar('ai-chat', 'color')}; -} - -@include b(markdown-message-footer) { - margin-top: 6px; @include e(action-item) { padding: 6px; + margin-left: 6px; font-size: 12px; border: 1px solid #{getCssVar('ai-chat', 'border-color')}; border-radius: 15px; + &:nth-child(1) { + margin-left: 0; + } + &:hover { color: #{getCssVar('ai-chat', 'hover-color')}; cursor: pointer; @@ -67,3 +59,8 @@ } } } + +@include b(markdown-message-header-action-wrapper) { + display: flex; + align-items: center; +} 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 2667eda..453e1be 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.tsx +++ b/src/components/chat-message-item/markdown-message/markdown-message.tsx @@ -3,7 +3,7 @@ import { useSignal } from '@preact/signals'; import { useEffect } from 'preact/hooks'; import { Namespace, createUUID } from '../../../utils'; import { IChatMessage } from '../../../interface'; -import { FillSvg } from '../../../icons'; +import { FillSvg, DeleteSvg, RefreshSvg, CopySvg } from '../../../icons'; import { AiChatController } from '../../../controller'; import './markdown-message.scss'; @@ -52,29 +52,61 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { }); }, []); - const onClick = () => { + const onBackfill = () => { props.controller.backfill(message); }; + const onDeleteMessage = () => { + props.controller.deleteMessage(message); + }; + + const onRefreshMessage = () => { + props.controller.refreshMessage(message); + }; + + const onCopyMessage = () => { + props.controller.copyMessage(message); + }; + return (
AI:
-
-
-
-
-
-
+
+
+ +
+
+ +
+
+ +
+
+
+
+
); }; diff --git a/src/controller/ai-chat/ai-chat.controller.ts b/src/controller/ai-chat/ai-chat.controller.ts index 272a01c..3dbe0af 100644 --- a/src/controller/ai-chat/ai-chat.controller.ts +++ b/src/controller/ai-chat/ai-chat.controller.ts @@ -2,6 +2,7 @@ import { Signal, signal } from '@preact/signals'; import { ChatMessage } from '../../entity'; import { IChatMessage, IChatOptions } from '../../interface'; import { createUUID } from '../../utils'; +import { TextUtil } from '../../utils/util/util'; /** * 聊天逻辑控制器 @@ -122,4 +123,45 @@ export class AiChatController { this.opts.action('backfill', message); } } + + /** + * 删除指定消息 + * + * @param {IChatMessage} message + * @memberof AiChatController + */ + deleteMessage(message: IChatMessage): void { + const i = this.messages.value.findIndex( + item => item.messageid === message.messageid, + ); + if (i !== -1) { + this.messages.value.splice(i, 1); + this.messages.value = [...this.messages.value]; + } + } + + /** + * 刷新当前消息 + * + * @memberof AiChatController + */ + async refreshMessage(message: IChatMessage) { + this.deleteMessage(message); + await this.opts.question( + this.messages.value + .filter(item => item.type !== 'ERROR') + .map(item => item._origin), + ); + } + + /** + * 复制消息 + * + * @param {IChatMessage} message + * @memberof AiChatController + */ + copyMessage(message: IChatMessage) { + const text = message.content; + TextUtil.copy(text); + } } diff --git a/src/icons/close-full-screen-svg.tsx b/src/icons/close-full-screen-svg.tsx new file mode 100644 index 0000000..047e3b5 --- /dev/null +++ b/src/icons/close-full-screen-svg.tsx @@ -0,0 +1,6 @@ +// 关闭图标 +export const CloseFullScreenSvg = () => ( + + + +); diff --git a/src/icons/close-svg.tsx b/src/icons/close-svg.tsx index c9db93c..7bb3c36 100644 --- a/src/icons/close-svg.tsx +++ b/src/icons/close-svg.tsx @@ -1,5 +1,5 @@ // 关闭图标 -export const CloseSgv = () => ( +export const CloseSvg = () => ( diff --git a/src/icons/copy-svg.tsx b/src/icons/copy-svg.tsx new file mode 100644 index 0000000..cf0d4b6 --- /dev/null +++ b/src/icons/copy-svg.tsx @@ -0,0 +1,6 @@ +// 复制图标 +export const CopySvg = () => ( + + + +); diff --git a/src/icons/delete-svg.tsx b/src/icons/delete-svg.tsx new file mode 100644 index 0000000..164c14f --- /dev/null +++ b/src/icons/delete-svg.tsx @@ -0,0 +1,6 @@ +// 删除图标 +export const DeleteSvg = () => ( + + + +); diff --git a/src/icons/full-screen-svg.tsx b/src/icons/full-screen-svg.tsx new file mode 100644 index 0000000..3f56a24 --- /dev/null +++ b/src/icons/full-screen-svg.tsx @@ -0,0 +1,5 @@ +export const FullScreenSvg = () => ( + + + +); diff --git a/src/icons/index.ts b/src/icons/index.ts index 5159dd9..e29f0c1 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -1,3 +1,8 @@ -export { CloseSgv } from './close-svg'; +export { CloseSvg } from './close-svg'; export { FillSvg } from './fill-svg'; export { SendSvg } from './send-svg'; +export { DeleteSvg } from './delete-svg'; +export { RefreshSvg } from './refresh-svg'; +export { CopySvg } from './copy-svg'; +export { FullScreenSvg } from './full-screen-svg'; +export { CloseFullScreenSvg } from './close-full-screen-svg'; diff --git a/src/icons/refresh-svg.tsx b/src/icons/refresh-svg.tsx new file mode 100644 index 0000000..d962d8f --- /dev/null +++ b/src/icons/refresh-svg.tsx @@ -0,0 +1,6 @@ +// 刷新图标 +export const RefreshSvg = () => ( + + + +); diff --git a/src/utils/util/util.ts b/src/utils/util/util.ts index 3368a95..a7e92bb 100644 --- a/src/utils/util/util.ts +++ b/src/utils/util/util.ts @@ -20,3 +20,35 @@ function S4(): string { export function createUUID(): string { return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`; } + +export class TextUtil { + /** + * input元素,用于存储拷贝的文本 + * + * @author zhanghengfeng + * @date 2023-08-31 20:08:06 + * @private + * @type {(HTMLInputElement | null)} + */ + static inputElement: HTMLInputElement | null = null; + + /** + * 拷贝文本 + * + * @author zhanghengfeng + * @date 2023-08-31 11:08:51 + * @param {string} value + * @return {*} {boolean} + */ + static copy(value: string): boolean { + if (!this.inputElement) { + this.inputElement = document.createElement('input'); + this.inputElement.style.position = 'absolute'; + this.inputElement.style.left = '-9999px'; + document.body.appendChild(this.inputElement); + } + this.inputElement.value = value; + this.inputElement.select(); + return document.execCommand('copy'); + } +} -- Gitee From 32ea55adda508b9e351a02a92276cd193c47e182 Mon Sep 17 00:00:00 2001 From: jianglinjun Date: Tue, 7 May 2024 19:54:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat-container/chat-container.scss | 2 +- .../chat-container/chat-container.tsx | 50 +++++++++---------- src/components/chat-input/chat-input.tsx | 1 + .../markdown-message/markdown-message.scss | 1 + .../markdown-message/markdown-message.tsx | 10 ++-- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/components/chat-container/chat-container.scss b/src/components/chat-container/chat-container.scss index 66cb1d0..bd59086 100644 --- a/src/components/chat-container/chat-container.scss +++ b/src/components/chat-container/chat-container.scss @@ -44,7 +44,7 @@ $ai-chat: ( flex-shrink: 0; align-items: center; justify-content: space-between; - height: 36px; + height: 60px; border-bottom: 1px solid #{getCssVar('ai-chat', 'border-color')}; } diff --git a/src/components/chat-container/chat-container.tsx b/src/components/chat-container/chat-container.tsx index 1d9047e..eaff679 100644 --- a/src/components/chat-container/chat-container.tsx +++ b/src/components/chat-container/chat-container.tsx @@ -74,11 +74,11 @@ export class ChatContainer extends Component< calcStyle() { return { - left: `${this.data.x}px`, - top: `${this.data.y}px`, - height: `${this.data.height}px`, + right: `0px`, + top: `0px`, + height: `100vh`, width: `${this.data.width}px`, - 'z-index': '1000px', + 'z-index': '1000', }; } @@ -101,32 +101,32 @@ export class ChatContainer extends Component< this.setStyle(); const state = this.data; // 注册窗口拖拽 - interact(this.dragHandle.current!).draggable({ - modifiers: [ - interact.modifiers.restrictRect({ - restriction: document.body, - endOnly: true, - }), - ], - cursorChecker: () => { - return 'move'; - }, - listeners: { - move: event => { - // 计算偏移量保持位置 - state.x += event.dx; - state.y += event.dy; - this.setStyle(); - }, - }, - }); + // interact(this.dragHandle.current!).draggable({ + // modifiers: [ + // interact.modifiers.restrictRect({ + // restriction: document.body, + // endOnly: true, + // }), + // ], + // cursorChecker: () => { + // return 'move'; + // }, + // listeners: { + // move: event => { + // // 计算偏移量保持位置 + // state.x += event.dx; + // state.y += event.dy; + // this.setStyle(); + // }, + // }, + // }); // 注册窗口大小变更 interact(this.containerRef.current!).resizable({ // 可拖拽的边缘 edges: { - top: true, + top: false, right: true, - bottom: true, + bottom: false, left: true, }, margin: 6, diff --git a/src/components/chat-input/chat-input.tsx b/src/components/chat-input/chat-input.tsx index 10c3f07..78c3453 100644 --- a/src/components/chat-input/chat-input.tsx +++ b/src/components/chat-input/chat-input.tsx @@ -75,6 +75,7 @@ export const ChatInput = (props: ChatInputProps) => { className={ns.e('textarea')} type='text' wrap='off' + rows={6} autoCorrect='off' autoCapitalize='off' autoComplete='off' 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 80b1b97..f569227 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.scss +++ b/src/components/chat-message-item/markdown-message/markdown-message.scss @@ -21,6 +21,7 @@ margin-bottom: 8px; @include e(caption) { + flex: 0; width: auto; min-width: 32px; min-height: 32px; 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 453e1be..4d3a94d 100644 --- a/src/components/chat-message-item/markdown-message/markdown-message.tsx +++ b/src/components/chat-message-item/markdown-message/markdown-message.tsx @@ -71,32 +71,32 @@ export const MarkdownMessage = (props: MarkdownMessageProps) => { return (
-
AI:
+
AI
-- Gitee