@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
432 lines • 14.4 kB
JavaScript
import * as React from 'react';
import BaseComponent from '../_base/baseComponent';
import cls from "classnames";
import PropTypes from 'prop-types';
import InputBox from './inputBox';
import '@douyinfe/semi-foundation/lib/es/chat/chat.css';
import Hint from './hint';
import { IconChevronDown, IconDisc } from '@douyinfe/semi-icons';
import ChatContent from './chatContent';
import { getDefaultPropsFromGlobalConfig } from '../_utils';
import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/chat/constants';
import ChatFoundation from '@douyinfe/semi-foundation/lib/es/chat/foundation';
import LocaleConsumer from "../locale/localeConsumer";
import { Button } from '../index';
const prefixCls = cssClasses.PREFIX;
const {
CHAT_ALIGN,
MODE,
SEND_HOT_KEY,
MESSAGE_STATUS
} = strings;
class Chat extends BaseComponent {
constructor(props) {
super(props);
// dragStatus: Whether the component contains the dragged object
this.dragStatus = false;
this.resetMessage = () => {
this.foundation.resetMessage(null);
};
this.clearContext = () => {
this.foundation.clearContext(null);
};
this.scrollToBottom = animation => {
if (animation) {
this.foundation.scrollToBottomWithAnimation();
} else {
this.foundation.scrollToBottomImmediately();
}
};
this.sendMessage = (content, attachment) => {
this.foundation.onMessageSend(content, attachment);
};
this.containerScroll = e => {
this.scrollTargetRef.current = e.target;
if (e.target !== e.currentTarget) {
return;
}
this.foundation.containerScroll(e);
};
this.containerRef = /*#__PURE__*/React.createRef();
this.uploadRef = /*#__PURE__*/React.createRef();
this.dropAreaRef = /*#__PURE__*/React.createRef();
this.wheelEventHandler = null;
this.foundation = new ChatFoundation(this.adapter);
this.scrollTargetRef = /*#__PURE__*/React.createRef();
this.state = {
backBottomVisible: false,
chats: [],
cacheHints: [],
wheelScroll: false,
uploadAreaVisible: false
};
}
get adapter() {
return Object.assign(Object.assign({}, super.adapter), {
getContainerRef: () => {
var _a;
return (_a = this.containerRef) === null || _a === void 0 ? void 0 : _a.current;
},
setWheelScroll: flag => {
this.setState({
wheelScroll: flag
});
},
notifyChatsChange: chats => {
const {
onChatsChange
} = this.props;
onChatsChange && onChatsChange(chats);
},
notifyLikeMessage: message => {
const {
onMessageGoodFeedback
} = this.props;
onMessageGoodFeedback && onMessageGoodFeedback(message);
},
notifyDislikeMessage: message => {
const {
onMessageBadFeedback
} = this.props;
onMessageBadFeedback && onMessageBadFeedback(message);
},
notifyCopyMessage: message => {
const {
onMessageCopy
} = this.props;
onMessageCopy && onMessageCopy(message);
},
notifyClearContext: () => {
const {
onClear
} = this.props;
onClear && onClear();
},
notifyMessageSend: (content, attachment) => {
const {
onMessageSend
} = this.props;
onMessageSend && onMessageSend(content, attachment);
},
notifyInputChange: props => {
const {
onInputChange
} = this.props;
onInputChange && onInputChange(props);
},
setBackBottomVisible: visible => {
this.setState(state => {
if (state.backBottomVisible !== visible) {
return {
backBottomVisible: visible
};
}
return null;
});
},
registerWheelEvent: () => {
this.adapter.unRegisterWheelEvent();
const containerElement = this.containerRef.current;
if (!containerElement) {
return;
}
this.wheelEventHandler = e => {
var _a;
/**
* Why use this.scrollTargetRef.current and wheel's currentTarget target comparison?
* Both scroll and wheel events are on the container
* his.scrollTargetRef.current is the object where scrolling actually occurs
* wheel's currentTarget is the container,
* Only when the wheel event occurs and there is scroll, the following logic(show scroll bar) needs to be executed
*/
if (((_a = this.scrollTargetRef) === null || _a === void 0 ? void 0 : _a.current) !== e.currentTarget) {
return;
}
this.adapter.setWheelScroll(true);
this.adapter.unRegisterWheelEvent();
};
containerElement.addEventListener('wheel', this.wheelEventHandler);
},
unRegisterWheelEvent: () => {
if (this.wheelEventHandler) {
const containerElement = this.containerRef.current;
if (!containerElement) {
return;
} else {
containerElement.removeEventListener('wheel', this.wheelEventHandler);
}
this.wheelEventHandler = null;
}
},
notifyStopGenerate: e => {
const {
onStopGenerator
} = this.props;
onStopGenerator && onStopGenerator(e);
},
notifyHintClick: hint => {
const {
onHintClick
} = this.props;
onHintClick && onHintClick(hint);
},
setUploadAreaVisible: visible => {
this.setState({
uploadAreaVisible: visible
});
},
manualUpload: file => {
const uploadComponent = this.uploadRef.current;
if (uploadComponent) {
uploadComponent.insert(file);
}
},
getDropAreaElement: () => {
var _a;
return (_a = this.dropAreaRef) === null || _a === void 0 ? void 0 : _a.current;
},
getDragStatus: () => this.dragStatus,
setDragStatus: status => {
this.dragStatus = status;
}
});
}
static getDerivedStateFromProps(nextProps, prevState) {
const {
chats,
hints
} = nextProps;
const newState = {};
if (chats !== prevState.chats) {
newState.chats = chats !== null && chats !== void 0 ? chats : [];
}
if (hints !== prevState.cacheHints) {
newState.cacheHints = hints;
}
if (Object.keys(newState).length) {
return newState;
}
return null;
}
componentDidMount() {
this.foundation.init();
}
componentDidUpdate(prevProps, prevState, snapshot) {
const {
chats: newChats,
hints: newHints
} = this.props;
const {
chats: oldChats,
cacheHints
} = prevState;
const {
wheelScroll
} = this.state;
let shouldScroll = false;
if (newChats !== oldChats) {
if (Array.isArray(newChats) && Array.isArray(oldChats)) {
const newLastChat = newChats[newChats.length - 1];
const oldLastChat = oldChats[oldChats.length - 1];
if (newChats.length > oldChats.length) {
if (oldChats.length === 0 || newLastChat.id !== oldLastChat.id) {
shouldScroll = true;
}
} else if (newChats.length === oldChats.length && newChats.length && (newLastChat.status !== 'complete' || newLastChat.status !== oldLastChat.status)) {
shouldScroll = true;
}
}
}
if (newHints !== cacheHints) {
if (newHints.length > cacheHints.length) {
shouldScroll = true;
}
}
if (!wheelScroll && shouldScroll) {
this.foundation.scrollToBottomImmediately();
}
}
componentWillUnmount() {
this.foundation.destroy();
}
render() {
const {
topSlot,
bottomSlot,
roleConfig,
hints,
onChatsChange,
onMessageCopy,
renderInputArea,
chatBoxRenderConfig,
align,
renderHintBox,
style,
className,
showStopGenerate,
customMarkDownComponents,
mode,
showClearContext,
placeholder,
inputBoxCls,
inputBoxStyle,
hintStyle,
hintCls,
uploadProps,
uploadTipProps,
sendHotKey,
renderDivider,
markdownRenderProps,
enableUpload
} = this.props;
const {
backBottomVisible,
chats,
wheelScroll,
uploadAreaVisible
} = this.state;
let showStopGenerateFlag = false;
const lastChat = chats.length > 0 && chats[chats.length - 1];
let disableSend = false;
if (lastChat && showStopGenerate) {
const lastChatOnGoing = (lastChat === null || lastChat === void 0 ? void 0 : lastChat.status) && [MESSAGE_STATUS.LOADING, MESSAGE_STATUS.INCOMPLETE].includes(lastChat === null || lastChat === void 0 ? void 0 : lastChat.status);
disableSend = lastChatOnGoing;
showStopGenerate && (showStopGenerateFlag = lastChatOnGoing);
}
const {
dragUpload,
clickUpload,
pasteUpload
} = this.foundation.getUploadProps(enableUpload);
const dragEventHandlers = dragUpload ? {
onDragOver: this.foundation.handleDragOver,
onDragStart: this.foundation.handleDragStart,
onDragEnd: this.foundation.handleDragEnd
} : {};
return /*#__PURE__*/React.createElement("div", Object.assign({
className: cls(`${prefixCls}`, className),
style: style
}, dragEventHandlers), dragUpload && uploadAreaVisible && /*#__PURE__*/React.createElement("div", {
ref: this.dropAreaRef,
className: `${prefixCls}-dropArea`,
onDragOver: this.foundation.handleContainerDragOver,
onDrop: this.foundation.handleContainerDrop,
onDragLeave: this.foundation.handleContainerDragLeave
}, /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-dropArea-text`
}, /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "Chat"
}, locale => locale['dropAreaText']))), /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-inner`
}, topSlot, /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-content`
}, /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-container`, {
'semi-chat-container-scroll-hidden': !wheelScroll
}),
onScroll: this.containerScroll,
ref: this.containerRef
}, /*#__PURE__*/React.createElement(ChatContent, {
align: align,
mode: mode,
chats: chats,
roleConfig: roleConfig,
customMarkDownComponents: customMarkDownComponents,
onMessageDelete: this.foundation.deleteMessage,
onChatsChange: onChatsChange,
onMessageBadFeedback: this.foundation.dislikeMessage,
onMessageGoodFeedback: this.foundation.likeMessage,
onMessageReset: this.foundation.resetMessage,
onMessageCopy: onMessageCopy,
chatBoxRenderConfig: chatBoxRenderConfig,
renderDivider: renderDivider,
markdownRenderProps: markdownRenderProps
}), !!(hints === null || hints === void 0 ? void 0 : hints.length) && /*#__PURE__*/React.createElement(Hint, {
className: hintCls,
style: hintStyle,
value: hints,
onHintClick: this.foundation.onHintClick,
renderHintBox: renderHintBox
}))), backBottomVisible && !showStopGenerateFlag && (/*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-action`
}, /*#__PURE__*/React.createElement(Button, {
className: `${prefixCls}-action-content ${prefixCls}-action-backBottom`,
icon: /*#__PURE__*/React.createElement(IconChevronDown, {
size: "extra-large"
}),
type: "tertiary",
onClick: this.foundation.scrollToBottomWithAnimation
}))), showStopGenerateFlag && (/*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-action`
}, /*#__PURE__*/React.createElement(Button, {
className: `${prefixCls}-action-content ${prefixCls}-action-stop`,
icon: /*#__PURE__*/React.createElement(IconDisc, {
size: "extra-large"
}),
type: "tertiary",
onClick: this.foundation.stopGenerate
}, /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "Chat"
}, locale => locale['stop'])))), /*#__PURE__*/React.createElement(InputBox, {
showClearContext: showClearContext,
uploadRef: this.uploadRef,
manualUpload: this.adapter.manualUpload,
style: inputBoxStyle,
className: inputBoxCls,
placeholder: placeholder,
disableSend: disableSend,
onClearContext: this.foundation.clearContext,
onSend: this.foundation.onMessageSend,
onInputChange: this.foundation.onInputChange,
renderInputArea: renderInputArea,
uploadProps: uploadProps,
uploadTipProps: uploadTipProps,
sendHotKey: sendHotKey,
clickUpload: clickUpload,
pasteUpload: pasteUpload
}), bottomSlot));
}
}
Chat.__SemiComponentName__ = "Chat";
Chat.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
roleConfig: PropTypes.object,
chats: PropTypes.array,
hints: PropTypes.array,
renderHintBox: PropTypes.func,
onChatsChange: PropTypes.func,
align: PropTypes.string,
chatBoxRenderConfig: PropTypes.object,
customMarkDownComponents: PropTypes.object,
onClear: PropTypes.func,
onMessageDelete: PropTypes.func,
onMessageReset: PropTypes.func,
onMessageCopy: PropTypes.func,
onMessageGoodFeedback: PropTypes.func,
onMessageBadFeedback: PropTypes.func,
inputContentConvert: PropTypes.func,
onMessageSend: PropTypes.func,
InputBoxStyle: PropTypes.object,
inputBoxCls: PropTypes.string,
renderFullInputBox: PropTypes.func,
placeholder: PropTypes.string,
topSlot: PropTypes.node || PropTypes.array,
bottomSlot: PropTypes.node || PropTypes.array,
showStopGenerate: PropTypes.bool,
showClearContext: PropTypes.bool,
hintStyle: PropTypes.object,
hintCls: PropTypes.string,
uploadProps: PropTypes.object,
uploadTipProps: PropTypes.object,
mode: PropTypes.string,
markdownRenderProps: PropTypes.object
};
Chat.defaultProps = getDefaultPropsFromGlobalConfig(Chat.__SemiComponentName__, {
align: CHAT_ALIGN.LEFT_RIGHT,
showStopGenerate: false,
mode: MODE.BUBBLE,
showClearContext: false,
sendHotKey: SEND_HOT_KEY.ENTER
});
export default Chat;