chatBoxAction.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import React, { PureComponent, ReactNode } from 'react';
  2. import PropTypes from 'prop-types';
  3. import type { ChatBoxProps, DefaultActionNodeObj, RenderActionProps } from '../interface';
  4. import { IconThumbUpStroked,
  5. IconDeleteStroked,
  6. IconCopyStroked,
  7. IconLikeThumb,
  8. IconRedoStroked
  9. } from '@douyinfe/semi-icons';
  10. import { BaseComponent, Button, Popconfirm } from '../../index';
  11. import copy from 'copy-text-to-clipboard';
  12. import { cssClasses, strings } from '@douyinfe/semi-foundation/chat/constants';
  13. import ChatBoxActionFoundation, { ChatBoxActionAdapter } from '@douyinfe/semi-foundation/chat/chatBoxActionFoundation';
  14. import LocaleConsumer from "../../locale/localeConsumer";
  15. import { Locale } from "../../locale/interface";
  16. import cls from 'classnames';
  17. const { PREFIX_CHAT_BOX_ACTION } = cssClasses;
  18. const { ROLE, MESSAGE_STATUS } = strings;
  19. interface ChatBoxActionProps extends ChatBoxProps {
  20. customRenderFunc?: (props: RenderActionProps) => ReactNode
  21. }
  22. interface ChatBoxActionState {
  23. visible: boolean;
  24. showAction: boolean
  25. }
  26. class ChatBoxAction extends BaseComponent<ChatBoxActionProps, ChatBoxActionState> {
  27. static propTypes = {
  28. role: PropTypes.object,
  29. message: PropTypes.object,
  30. showReset: PropTypes.bool,
  31. onMessageBadFeedback: PropTypes.func,
  32. onMessageGoodFeedback: PropTypes.func,
  33. onMessageCopy: PropTypes.func,
  34. onChatsChange: PropTypes.func,
  35. onMessageDelete: PropTypes.func,
  36. onMessageReset: PropTypes.func,
  37. customRenderFunc: PropTypes.func,
  38. }
  39. copySuccessNode: ReactNode;
  40. foundation: ChatBoxActionFoundation;
  41. containerRef: React.RefObject<HTMLDivElement>;
  42. popconfirmTriggerRef: React.RefObject<HTMLSpanElement>;
  43. clickOutsideHandler: any;
  44. constructor(props: ChatBoxProps) {
  45. super(props);
  46. this.foundation = new ChatBoxActionFoundation(this.adapter);
  47. this.copySuccessNode = null;
  48. this.state = {
  49. visible: false,
  50. showAction: false,
  51. };
  52. this.clickOutsideHandler = null;
  53. this.containerRef = React.createRef<HTMLDivElement>();
  54. this.popconfirmTriggerRef = React.createRef<HTMLSpanElement>();
  55. }
  56. componentDidMount(): void {
  57. this.copySuccessNode = <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
  58. {(locale: Locale["Chat"]) => locale['copySuccess']}
  59. </LocaleConsumer>;
  60. }
  61. componentWillUnmount(): void {
  62. this.foundation.destroy();
  63. }
  64. get adapter(): ChatBoxActionAdapter<ChatBoxActionProps, ChatBoxActionState> {
  65. return {
  66. ...super.adapter,
  67. notifyDeleteMessage: () => {
  68. const { message, onMessageDelete } = this.props;
  69. onMessageDelete?.(message);
  70. },
  71. notifyMessageCopy: () => {
  72. const { message, onMessageCopy } = this.props;
  73. onMessageCopy?.(message);
  74. },
  75. copyToClipboardAndToast: () => {
  76. const { message = {}, toast } = this.props;
  77. if (typeof message.content === 'string') {
  78. copy(message.content);
  79. } else if (Array.isArray(message.content)) {
  80. const content = message.content?.map(item => item.text).join('');
  81. copy(content);
  82. }
  83. toast.success({
  84. content: this.copySuccessNode
  85. });
  86. },
  87. notifyLikeMessage: () => {
  88. const { message, onMessageGoodFeedback } = this.props;
  89. onMessageGoodFeedback?.(message);
  90. },
  91. notifyDislikeMessage: () => {
  92. const { message, onMessageBadFeedback } = this.props;
  93. onMessageBadFeedback?.(message);
  94. },
  95. notifyResetMessage: () => {
  96. const { message, onMessageReset } = this.props;
  97. onMessageReset?.(message);
  98. },
  99. setVisible: (visible) => {
  100. this.setState({ visible });
  101. },
  102. setShowAction: (showAction) => {
  103. this.setState({ showAction });
  104. },
  105. registerClickOutsideHandler: (cb: () => void) => {
  106. if (this.clickOutsideHandler) {
  107. this.adapter.unregisterClickOutsideHandler();
  108. }
  109. this.clickOutsideHandler = (e: React.MouseEvent): any => {
  110. let el = this.popconfirmTriggerRef && this.popconfirmTriggerRef.current;
  111. const target = e.target as Element;
  112. const path = (e as any).composedPath && (e as any).composedPath() || [target];
  113. if (
  114. el && !(el as any).contains(target) &&
  115. ! path.includes(el)
  116. ) {
  117. cb();
  118. }
  119. };
  120. window.addEventListener('mousedown', this.clickOutsideHandler);
  121. },
  122. unregisterClickOutsideHandler: () => {
  123. if (this.clickOutsideHandler) {
  124. window.removeEventListener('mousedown', this.clickOutsideHandler);
  125. this.clickOutsideHandler = null;
  126. }
  127. },
  128. };
  129. }
  130. copyNode = () => {
  131. return <Button
  132. key={'copy'}
  133. theme='borderless'
  134. icon={<IconCopyStroked />}
  135. type='tertiary'
  136. onClick={this.foundation.copyMessage}
  137. className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
  138. />;
  139. }
  140. likeNode = () => {
  141. const { message = {} } = this.props;
  142. const { like } = message;
  143. return <Button
  144. key={'like'}
  145. theme='borderless'
  146. icon={like ? <IconLikeThumb /> : <IconThumbUpStroked /> }
  147. type='tertiary'
  148. className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
  149. onClick={this.foundation.likeMessage}
  150. />;
  151. }
  152. dislikeNode = () => {
  153. const { message = {} } = this.props;
  154. const { dislike } = message;
  155. return <Button
  156. theme='borderless'
  157. key={'dislike'}
  158. icon={dislike ? <IconLikeThumb className={`${PREFIX_CHAT_BOX_ACTION}-icon-flip`} /> : <IconThumbUpStroked className={'semi-chat-chatBox-action-icon-flip'} />}
  159. type='tertiary'
  160. className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
  161. onClick={this.foundation.dislikeMessage}
  162. />;
  163. }
  164. resetNode = () => {
  165. return <Button
  166. key={'reset'}
  167. theme='borderless'
  168. icon={<IconRedoStroked className={`${PREFIX_CHAT_BOX_ACTION}-icon-redo`}/>}
  169. type='tertiary'
  170. onClick={this.foundation.resetMessage}
  171. className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
  172. />;
  173. }
  174. deleteNode = () => {
  175. const deleteMessage = (<LocaleConsumer<Locale["Chat"]> componentName="Chat" >
  176. {(locale: Locale["Chat"]) => locale['deleteConfirm']}
  177. </LocaleConsumer>);
  178. return (<Popconfirm
  179. trigger="custom"
  180. visible={this.state.visible}
  181. key={'delete'}
  182. title={deleteMessage}
  183. onConfirm={this.foundation.deleteMessage}
  184. onCancel={this.foundation.hideDeletePopup}
  185. position='top'
  186. >
  187. <span
  188. ref={this.popconfirmTriggerRef}
  189. className={`${PREFIX_CHAT_BOX_ACTION}-delete-wrap`}
  190. >
  191. <Button
  192. theme='borderless'
  193. icon={<IconDeleteStroked />}
  194. type='tertiary'
  195. className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
  196. onClick={this.foundation.showDeletePopup}
  197. />
  198. </span>
  199. </Popconfirm>);
  200. }
  201. render() {
  202. const { message = {}, lastChat } = this.props;
  203. const { showAction } = this.state;
  204. const { role, status = MESSAGE_STATUS.COMPLETE } = message;
  205. const complete = status === MESSAGE_STATUS.COMPLETE ;
  206. const showFeedback = role !== ROLE.USER && complete;
  207. const showReset = lastChat && role === ROLE.ASSISTANT;
  208. const finished = status !== MESSAGE_STATUS.LOADING && status !== MESSAGE_STATUS.INCOMPLETE;
  209. const wrapCls = cls(PREFIX_CHAT_BOX_ACTION, {
  210. [`${PREFIX_CHAT_BOX_ACTION}-show`]: showReset && finished || showAction,
  211. [`${PREFIX_CHAT_BOX_ACTION}-hidden`]: !finished,
  212. });
  213. const { customRenderFunc } = this.props;
  214. if (customRenderFunc) {
  215. const actionNodes = [];
  216. const actionNodeObj = {} as DefaultActionNodeObj;
  217. if (complete) {
  218. const copyNode = this.copyNode();
  219. actionNodes.push(copyNode);
  220. actionNodeObj.copyNode = copyNode;
  221. }
  222. if (showFeedback) {
  223. const likeNode = this.likeNode();
  224. actionNodes.push(likeNode);
  225. actionNodeObj.likeNode = likeNode;
  226. const dislikeNode = this.dislikeNode();
  227. actionNodes.push(dislikeNode);
  228. actionNodeObj.dislikeNode = dislikeNode;
  229. }
  230. if (showReset) {
  231. const resetNode = this.resetNode();
  232. actionNodes.push(resetNode);
  233. actionNodeObj.resetNode = resetNode;
  234. }
  235. const deleteNode = this.deleteNode();
  236. actionNodes.push(deleteNode);
  237. actionNodeObj.deleteNode = deleteNode;
  238. return customRenderFunc({
  239. message,
  240. defaultActions: actionNodes,
  241. className: wrapCls,
  242. defaultActionsObj: actionNodeObj
  243. });
  244. }
  245. return <div className={wrapCls} ref={this.containerRef}>
  246. {complete && this.copyNode()}
  247. {showFeedback && this.likeNode()}
  248. {showFeedback && this.dislikeNode()}
  249. {showReset && this.resetNode()}
  250. {this.deleteNode()}
  251. </div>;
  252. }
  253. }
  254. export default ChatBoxAction;