index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import * as React from 'react';
  2. import BaseComponent from '../_base/baseComponent';
  3. import PropTypes from 'prop-types';
  4. import cls from "classnames";
  5. import "@douyinfe/semi-foundation/aiChatDialogue/aiChatDialogue.scss";
  6. import { ReasoningWidget } from './widgets/contentItem/reasoning';
  7. import { DialogueStepWidget } from './widgets/contentItem/dialogueStep';
  8. import { AnnotationWidget } from './widgets/contentItem/annotation';
  9. import DialogueItem from './Dialogue';
  10. import DialogueFoundation, { DialogueAdapter, Message } from '@douyinfe/semi-foundation/aiChatDialogue/foundation';
  11. import { AIChatDialogueProps } from './interface';
  12. import { getDefaultPropsFromGlobalConfig } from '../_utils';
  13. import { cssClasses, strings } from '@douyinfe/semi-foundation/aiChatDialogue/constants';
  14. import Hint from './widgets/dialogueHint';
  15. import { Button } from "../index";
  16. import { IconChevronDown } from '@douyinfe/semi-icons';
  17. export * from '@douyinfe/semi-foundation/aiChatDialogue/foundation';
  18. export * from './interface';
  19. export interface AIChatDialogueStates {
  20. chats?: Message[];
  21. selectedIds: Set<string>;
  22. cacheHints?: string[];
  23. backBottomVisible: boolean;
  24. wheelScroll: boolean
  25. }
  26. const { DIALOGUE_ALIGN, MODE } = strings;
  27. const { PREFIX } = cssClasses;
  28. class AIChatDialogue extends BaseComponent<AIChatDialogueProps, AIChatDialogueStates> {
  29. static __SemiComponentName__ = "AIChatDialogue";
  30. static Reasoning = ReasoningWidget;
  31. static Step = DialogueStepWidget;
  32. static Annotation = AnnotationWidget;
  33. foundation: DialogueFoundation;
  34. containerRef: React.RefObject<HTMLDivElement>;
  35. scrollTargetRef: React.RefObject<HTMLElement>;
  36. wheelEventHandler: any;
  37. static propTypes = {
  38. align: PropTypes.oneOf(['leftRight', 'leftAlign']),
  39. chats: PropTypes.array,
  40. className: PropTypes.string,
  41. disabledFileItemClick: PropTypes.bool,
  42. hints: PropTypes.array,
  43. hintCls: PropTypes.string,
  44. hintStyle: PropTypes.object,
  45. selecting: PropTypes.bool,
  46. markdownRenderProps: PropTypes.object,
  47. messageEditRender: PropTypes.func,
  48. mode: PropTypes.string,
  49. roleConfig: PropTypes.object,
  50. style: PropTypes.object,
  51. renderConfig: PropTypes.object,
  52. renderHintBox: PropTypes.func,
  53. renderDialogueContentItem: PropTypes.object,
  54. onAnnotationClick: PropTypes.func,
  55. onChatsChange: PropTypes.func,
  56. onFileClick: PropTypes.func,
  57. onImageClick: PropTypes.func,
  58. onHintClick: PropTypes.func,
  59. onMessageBadFeedback: PropTypes.func,
  60. onMessageCopy: PropTypes.func,
  61. onMessageDelete: PropTypes.func,
  62. onMessageEdit: PropTypes.func,
  63. onMessageGoodFeedback: PropTypes.func,
  64. onMessageReset: PropTypes.func,
  65. onMessageShare: PropTypes.func,
  66. onSelect: PropTypes.func,
  67. showReset: PropTypes.bool,
  68. showReference: PropTypes.bool,
  69. };
  70. static defaultProps = getDefaultPropsFromGlobalConfig(AIChatDialogue.__SemiComponentName__, {
  71. align: DIALOGUE_ALIGN.LEFT_RIGHT,
  72. mode: MODE.BUBBLE,
  73. selecting: false,
  74. disabledFileItemClick: false,
  75. showReset: true,
  76. showReference: false,
  77. })
  78. constructor(props: AIChatDialogueProps) {
  79. super(props);
  80. this.foundation = new DialogueFoundation(this.adapter);
  81. this.containerRef = React.createRef();
  82. this.scrollTargetRef = React.createRef();
  83. this.wheelEventHandler = null;
  84. this.state = {
  85. cacheHints: [],
  86. selectedIds: new Set<string>(),
  87. chats: [],
  88. backBottomVisible: false,
  89. wheelScroll: false,
  90. };
  91. this.onSelectOrRemove = this.onSelectOrRemove.bind(this);
  92. }
  93. get adapter(): DialogueAdapter<AIChatDialogueProps, AIChatDialogueStates> {
  94. return {
  95. ...super.adapter,
  96. getContainerRef: () => this.containerRef?.current,
  97. setWheelScroll: (flag: boolean) => {
  98. this.setState({
  99. wheelScroll: flag,
  100. });
  101. },
  102. updateSelected: (selectedIds: Set<string>) => {
  103. this.setState({ selectedIds });
  104. },
  105. notifySelect: (selectedIds: string[]) => {
  106. const { onSelect } = this.props;
  107. onSelect && onSelect(selectedIds);
  108. },
  109. notifyChatsChange: (chats: Message[]) => {
  110. const { onChatsChange } = this.props;
  111. onChatsChange && onChatsChange(chats);
  112. },
  113. notifyCopyMessage: (message: Message) => {
  114. const { onMessageCopy } = this.props;
  115. onMessageCopy && onMessageCopy(message);
  116. },
  117. notifyLikeMessage: (message: Message) => {
  118. const { onMessageGoodFeedback } = this.props;
  119. onMessageGoodFeedback && onMessageGoodFeedback(message);
  120. },
  121. notifyDislikeMessage: (message: Message) => {
  122. const { onMessageBadFeedback } = this.props;
  123. onMessageBadFeedback && onMessageBadFeedback(message);
  124. },
  125. notifyEditMessage: (message: Message) => {
  126. const { onMessageEdit } = this.props;
  127. onMessageEdit && onMessageEdit(message);
  128. },
  129. notifyHintClick: (hint: string) => {
  130. const { onHintClick } = this.props;
  131. onHintClick && onHintClick(hint);
  132. },
  133. setBackBottomVisible: (visible: boolean) => {
  134. this.setState((state) => {
  135. if (state.backBottomVisible !== visible) {
  136. return {
  137. backBottomVisible: visible,
  138. };
  139. }
  140. return null;
  141. });
  142. },
  143. registerWheelEvent: () => {
  144. this.adapter.unRegisterWheelEvent();
  145. const containerElement = this.containerRef.current;
  146. if (!containerElement) {
  147. return ;
  148. }
  149. this.wheelEventHandler = (e: any) => {
  150. /**
  151. * Why use this.scrollTargetRef.current and wheel's currentTarget target comparison?
  152. * Both scroll and wheel events are on the container
  153. * his.scrollTargetRef.current is the object where scrolling actually occurs
  154. * wheel's currentTarget is the container,
  155. * Only when the wheel event occurs and there is scroll, the following logic(show scroll bar) needs to be executed
  156. */
  157. if (this.scrollTargetRef?.current !== e.currentTarget) {
  158. return;
  159. }
  160. this.adapter.setWheelScroll(true);
  161. this.adapter.unRegisterWheelEvent();
  162. };
  163. containerElement.addEventListener('wheel', this.wheelEventHandler);
  164. },
  165. unRegisterWheelEvent: () => {
  166. if (this.wheelEventHandler) {
  167. const containerElement = this.containerRef.current;
  168. if (!containerElement) {
  169. return ;
  170. } else {
  171. containerElement.removeEventListener('wheel', this.wheelEventHandler);
  172. }
  173. this.wheelEventHandler = null;
  174. }
  175. },
  176. };
  177. }
  178. static getDerivedStateFromProps(nextProps: AIChatDialogueProps, prevState: AIChatDialogueStates) {
  179. const { chats, hints } = nextProps;
  180. const newState = {} as any;
  181. if (chats !== prevState.chats) {
  182. newState.chats = chats ?? [];
  183. }
  184. if (hints !== prevState.cacheHints) {
  185. newState.cacheHints = hints;
  186. }
  187. if (Object.keys(newState).length) {
  188. return newState;
  189. }
  190. return null;
  191. }
  192. componentDidMount(): void {
  193. this.foundation.init();
  194. }
  195. componentDidUpdate(prevProps: Readonly<AIChatDialogueProps>, prevState: Readonly<AIChatDialogueStates>, snapshot?: any): void {
  196. const { chats: newChats, hints: newHints } = this.props;
  197. const { chats: oldChats, cacheHints } = prevState;
  198. const { wheelScroll } = this.state;
  199. let shouldScroll = false;
  200. if (newChats.length > oldChats.length) {
  201. this.adapter.setWheelScroll(false);
  202. this.adapter.registerWheelEvent();
  203. this.foundation.scrollToBottomImmediately();
  204. }
  205. if (newChats !== oldChats) {
  206. this.foundation.handleChatsChange(newChats);
  207. if (Array.isArray(newChats) && Array.isArray(oldChats)) {
  208. const newLastChat = newChats[newChats.length - 1];
  209. const oldLastChat = oldChats[oldChats.length - 1];
  210. if (newChats.length > oldChats.length) {
  211. if (oldChats.length === 0 || newLastChat.id !== oldLastChat.id) {
  212. shouldScroll = true;
  213. }
  214. } else if (newChats.length === oldChats.length && newChats.length &&
  215. (newLastChat.status !== 'completed' || newLastChat.status !== oldLastChat.status)
  216. ) {
  217. shouldScroll = true;
  218. }
  219. }
  220. }
  221. if (newHints !== cacheHints) {
  222. if (newHints.length > cacheHints.length) {
  223. shouldScroll = true;
  224. }
  225. }
  226. if (!wheelScroll && shouldScroll) {
  227. this.foundation.scrollToBottomImmediately();
  228. }
  229. }
  230. componentWillUnmount(): void {
  231. this.foundation.destroy();
  232. }
  233. selectAll = () => {
  234. this.foundation.handleSelectAll();
  235. }
  236. deselectAll = () => {
  237. this.foundation.handleDeselectAll();
  238. }
  239. onSelectOrRemove(isChecked: boolean, item: string) {
  240. this.foundation.handleSelectOrRemove(isChecked, item);
  241. }
  242. scrollToBottom = (animation: boolean) => {
  243. if (animation) {
  244. this.foundation.scrollToBottomWithAnimation();
  245. } else {
  246. this.foundation.scrollToBottomImmediately();
  247. }
  248. }
  249. containerScroll = (e: React.UIEvent<HTMLDivElement>) => {
  250. (this.scrollTargetRef as any).current = e.target as HTMLElement;
  251. if (e.target !== e.currentTarget) {
  252. return;
  253. }
  254. this.foundation.containerScroll(e);
  255. }
  256. render() {
  257. const { roleConfig, onMessageBadFeedback, onMessageGoodFeedback, onMessageReset, onMessageEdit, onMessageDelete, onHintClick,
  258. selecting, hintCls, hintStyle, hints, renderHintBox, style, className, ...restProps } = this.props;
  259. const { selectedIds, chats, backBottomVisible, wheelScroll } = this.state;
  260. return (
  261. <div
  262. className={cls(`${PREFIX}`, className)}
  263. style={style}
  264. >
  265. <div
  266. className={cls(`${PREFIX}-list`, {
  267. [`${PREFIX}-list-scroll-hidden`]: !wheelScroll
  268. })}
  269. onScroll={this.containerScroll}
  270. ref={this.containerRef}
  271. >
  272. {chats.map((chat, index) => {
  273. const isLastChat = index === chats.length - 1;
  274. const continueSend = index > 0 && chat?.role === chats[index - 1]?.role;
  275. return (
  276. <DialogueItem
  277. key={chat.id}
  278. message={chat}
  279. role={roleConfig[chat.role]}
  280. onSelectChange={this.onSelectOrRemove}
  281. isSelected={selectedIds.has(chat.id)}
  282. roleConfig={roleConfig}
  283. onMessageBadFeedback={this.foundation.dislikeMessage}
  284. onMessageGoodFeedback={this.foundation.likeMessage}
  285. onMessageReset={this.foundation.resetMessage}
  286. onMessageEdit={this.foundation.editMessage}
  287. onMessageDelete={this.foundation.deleteMessage}
  288. isLastChat={isLastChat}
  289. // todo: 不太确定用户的需求场景,暂时设置成 false,如果用户有相关需求,转为一个对外提供的 api
  290. // todo: Not sure about the user's demand scenario, temporarily set it to false.
  291. // If the user has relevant needs, turn it into an external API
  292. continueSend={false}
  293. selecting={selecting}
  294. {...restProps}
  295. />
  296. );
  297. })}
  298. {
  299. !!hints?.length && <Hint
  300. className={hintCls}
  301. style={hintStyle}
  302. hints={hints}
  303. onHintClick={this.foundation.onHintClick}
  304. renderHintBox={renderHintBox}
  305. selecting={selecting}
  306. />
  307. }
  308. </div>
  309. {
  310. backBottomVisible && (<span className={`${PREFIX}-backBottom`}>
  311. <Button
  312. className={`${PREFIX}-backBottom-button`}
  313. icon={<IconChevronDown size="extra-large"/>}
  314. type="tertiary"
  315. onClick={this.foundation.scrollToBottomWithAnimation}
  316. />
  317. </span>)
  318. }
  319. </div>
  320. );
  321. }
  322. }
  323. export default AIChatDialogue;