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 {
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 ) {
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
};
}
}
}