diff --git a/src/components/chat-container/chat-container.scss b/src/components/chat-container/chat-container.scss index 66cb1d01add08c199b8f02a72c11514b2de5bc65..bd59086963854211ea6e8614f4f6006bb919f426 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 0837f60b13def378ab408502f48415e81926b6f8..eaff6796870f36ab5770edfd0d57b7a8103feb5e 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(); @@ -53,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', }; } @@ -80,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, @@ -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-input/chat-input.tsx b/src/components/chat-input/chat-input.tsx index 10c3f0731dfb06652e7e0adea0c995ccc333a457..78c3453bcc2b59dbfc9df477a92d4107239fb195 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 f73daa8cd7f85ee34db26bcf92147b9e9d721122..f569227f390231036d9bf459fbb20855d4e072c7 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,31 @@ @include b(markdown-message-header) { display: flex; + margin-bottom: 8px; @include e(caption) { - flex-grow: 1; + flex: 0; + 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 +60,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 2667eda54c9a3ab42ff3a6f7be6dac86404258f0..4d3a94d91cb4858bb7014d9a22696b7785406d68 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:
-
-
-
-
-
-
+
AI
+
+
+ +
+
+ +
+
+ +
+
+
+
+
); }; diff --git a/src/controller/ai-chat/ai-chat.controller.ts b/src/controller/ai-chat/ai-chat.controller.ts index 272a01c7bad98e2b3fa9b999070c0284c8e4474e..3dbe0affaceb544f66288ec666c47a4aff654599 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 0000000000000000000000000000000000000000..047e3b5d1a40db35268c646cd95017fc99a85407 --- /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 c9db93cfd239c23f6548abba1a8b15ead5784a57..7bb3c36df59894ac436a8dcc9e2241f7993e3fc7 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 0000000000000000000000000000000000000000..cf0d4b66a6ad34faf9e599ad55204cec746e4a36 --- /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 0000000000000000000000000000000000000000..164c14f85001dab6370b57269e044d53295812e9 --- /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 0000000000000000000000000000000000000000..3f56a24fdd25e4ca5a98cfd44ce8463739af626f --- /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 5159dd92a017018e79359b923767c1d5481be1f2..e29f0c13d6bd7f8cc7e7150d8925c0f2a02e273d 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 0000000000000000000000000000000000000000..d962d8f4793471069537b77c4b377e564a21f522 --- /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 3368a95dceda50833579cd91ba55fea853c72e80..a7e92bbe3e17335c53ad447c882b7af76a716b6b 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'); + } +}