import BaseFoundation, { DefaultAdapter } from "../base/foundation"; import { strings } from "./constants"; import { Animation } from '@fx1422/semi-animation'; import { debounce } from "lodash"; import { getUuidv4 } from "../utils/uuid"; import { handlePrevent } from "../utils/a11y"; const { PIC_PREFIX, PIC_SUFFIX_ARRAY, ROLE, SCROLL_ANIMATION_TIME, SHOW_SCROLL_GAP } = strings; export interface Content { type: 'text' | 'image_url' | 'file_url'; text?: string; image_url?: { url: string; [x: string]: any }; file_url?: { url: string; name: string; size: string; type: string; [x: string]: any } } export interface Message { role?: string; name?: string; id?: string; content?: string | Content[]; parentId?: string; createAt?: number; status?: 'loading' | 'incomplete' | 'complete' | 'error'; [x: string]: any } export interface EnableUploadProps { pasteUpload?: boolean; dragUpload?: boolean; clickUpload?: boolean } export interface ChatAdapter

, S = Record> extends DefaultAdapter { getContainerRef: () => HTMLDivElement; setWheelScroll: (flag: boolean) => void; notifyChatsChange: (chats: Message[]) => void; notifyLikeMessage: (message: Message) => void; notifyDislikeMessage: (message: Message) => void; notifyCopyMessage: (message: Message) => void; notifyClearContext: () => void; notifyMessageSend: (content: string, attachment: any[]) => void; notifyInputChange: (props: { inputValue: string; attachment: any[]}) => void; setBackBottomVisible: (visible: boolean) => void; registerWheelEvent: () => void; unRegisterWheelEvent: () => void; notifyStopGenerate: (e: any) => void; notifyHintClick: (hint: string) => void; setUploadAreaVisible: (visible: boolean) => void; manualUpload: (e: any) => void; getDropAreaElement: () => HTMLDivElement; getDragStatus: () => boolean; setDragStatus: (status: boolean) => void } export default class ChatFoundation

, S = Record> extends BaseFoundation, P, S> { animation: any; constructor(adapter: ChatAdapter) { super({ ...adapter }); } init = () => { this.scrollToBottomImmediately(); this._adapter.registerWheelEvent(); } destroy = () => { this.animation && this.animation.destroy(); this._adapter.unRegisterWheelEvent(); } stopGenerate = (e: any) => { this._adapter.notifyStopGenerate(e); } scrollToBottomImmediately = () => { const element = this._adapter.getContainerRef(); if (element) { element.scrollTop = element.scrollHeight; } } scrollToBottomWithAnimation = () => { const duration = SCROLL_ANIMATION_TIME; const element = this._adapter.getContainerRef(); if (!element) { return; } const from = element.scrollTop; const to = element.scrollHeight; this.animation = new Animation( { from: { scrollTop: from }, to: { scrollTop: to }, }, { duration, easing: 'easeInOutCubic' } ); this.animation.on('frame', ({ scrollTop }: { scrollTop: number }) => { element.scrollTop = scrollTop; }); this.animation.start(); } containerScroll = (e: any) => { this._persistEvent(e); const update = () => { this.getScroll(e.target); }; requestAnimationFrame(update); } getScroll: (target: any) => void = debounce((target: any) => { const scrollHeight = target.scrollHeight; const clientHeight = target.clientHeight; const scrollTop = target.scrollTop; const { backBottomVisible } = this.getStates(); if (scrollHeight - scrollTop - clientHeight <= SHOW_SCROLL_GAP) { if (backBottomVisible) { this._adapter.setBackBottomVisible(false); } } else { if (!backBottomVisible) { this._adapter.setBackBottomVisible(true); } } return scroll; }, 100) clearContext = (e: any) => { const { chats } = this.getStates(); if (chats[chats.length - 1].role === ROLE.DIVIDER) { return; } const dividerMessage = { role: ROLE.DIVIDER, id: getUuidv4(), createAt: Date.now(), }; const newChats = [...chats, dividerMessage]; this._adapter.notifyChatsChange(newChats); this._adapter.notifyClearContext(); } onMessageSend = (input: string, attachment: any[]) => { let content; if (Boolean(attachment) && attachment.length === 0) { content = input; } else { content = []; input && content.push({ type: 'text', text: input }); (attachment ?? []).map(item => { const { fileInstance, name = '', url, size } = item; const suffix = name.split('.').pop(); const isImg = fileInstance?.type?.startsWith(PIC_PREFIX) || PIC_SUFFIX_ARRAY.includes(suffix); if (isImg) { content.push({ type: 'image_url', image_url: { url: url } }); } else { content.push({ type: 'file_url', file_url: { url: url, name: name, size: size, type: fileInstance?.type } }); } }); } if (content) { const newMessage = { role: ROLE.USER, id: getUuidv4(), createAt: Date.now(), content, }; this._adapter.notifyChatsChange([...this.getStates().chats, newMessage]); } this._adapter.setWheelScroll(false); this._adapter.registerWheelEvent(); this._adapter.notifyMessageSend(input, attachment); } onHintClick = (hint: string) => { const { chats } = this.getStates(); const newMessage = { role: ROLE.USER, id: getUuidv4(), createAt: Date.now(), content: hint, }; const newChats = [...chats, newMessage]; this._adapter.notifyChatsChange(newChats); this._adapter.notifyHintClick(hint); } onInputChange = (props: { inputValue: string; attachment: any[]}) => { this._adapter.notifyInputChange(props as any); } deleteMessage = (message: Message) => { const { onMessageDelete, onChatsChange } = this.getProps(); const { chats } = this.getStates(); onMessageDelete?.(message); const newChats = chats.filter(item => item.id !== message.id); onChatsChange?.(newChats); } likeMessage = (message: Message) => { const { chats } = this.getStates(); this._adapter.notifyLikeMessage(message); const index = chats.findIndex(item => item.id === message.id); const newChat = { ...chats[index], like: !chats[index].like, dislike: false, }; const newChats = [...chats]; newChats.splice(index, 1, newChat); this._adapter.notifyChatsChange(newChats); } dislikeMessage = (message: Message) => { const { chats } = this.getStates(); this._adapter.notifyDislikeMessage(message); const index = chats.findIndex(item => item.id === message.id); const newChat = { ...chats[index], like: false, dislike: !chats[index].dislike, }; const newChats = [...chats]; newChats.splice(index, 1, newChat); this._adapter.notifyChatsChange(newChats); } resetMessage = (message: Message) => { const { chats } = this.getStates(); const lastMessage = chats[chats.length - 1]; const newLastChat = { ...lastMessage, status: 'loading', content: '', id: getUuidv4(), createAt: Date.now(), }; const newChats = chats.slice(0, -1).concat(newLastChat); this._adapter.notifyChatsChange(newChats); const { onMessageReset } = this.getProps(); onMessageReset?.(message); } handleDragOver = (e: any) => { const dragStatus = this._adapter.getDragStatus(); if (dragStatus) { return; } this._adapter.setUploadAreaVisible(true); }; handleDragStart = (e: any) => { this._adapter.setDragStatus(true); } handleDragEnd = (e: any) => { this._adapter.setDragStatus(false); } handleContainerDragOver = (e: any) => { handlePrevent(e); } handleContainerDrop = (e) => { this._adapter.setUploadAreaVisible(false); this._adapter.manualUpload(e?.dataTransfer?.files); // 禁用默认实现,防止文件被打开 //Disable the default implementation, preventing files from being opened handlePrevent(e); } handleContainerDragLeave = (e: any) => { handlePrevent(e); // 鼠标移动至 container 的子元素,则不做任何操作 // If the mouse moves to the child element of container, no operation will be performed. const dropAreaElement = this._adapter.getDropAreaElement(); const enterTarget = e.relatedTarget; if (dropAreaElement.contains(enterTarget)) { return; } /** * 延迟隐藏 container ,防止父元素的 mouseOver 被触发,导致 container 无法隐藏 * Delay hiding of the container to prevent the parent element's mouseOver from being triggered, * causing the container to be unable to be hidden. */ setTimeout(() => { this._adapter.setUploadAreaVisible(false); }); } getUploadProps = (uploadProps?: boolean | EnableUploadProps) => { if (Object.prototype.toString.call(uploadProps) === '[object Object]') { const { dragUpload = true, clickUpload = true, pasteUpload = true } = uploadProps as EnableUploadProps; return { dragUpload: dragUpload, clickUpload: clickUpload, pasteUpload: pasteUpload }; } else if (typeof uploadProps === 'boolean') { return { dragUpload: uploadProps, clickUpload: uploadProps, pasteUpload: uploadProps }; } else { return { dragUpload: true, clickUpload: true, pasteUpload: true }; } } }