index.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. /* eslint-disable jsx-a11y/no-static-element-interactions */
  2. import React from 'react';
  3. import BaseComponent from '../_base/baseComponent';
  4. import { AIChatInputProps, AIChatInputState, Skill, Attachment, Reference, Content, LeftMenuChangeProps } from './interface';
  5. import { noop, isEqual } from 'lodash';
  6. import { cssClasses, numbers } from '@douyinfe/semi-foundation/aiChatInput/constants';
  7. import { Popover, Tooltip, Upload, Progress } from '../index';
  8. import { IconSendMsgStroked, IconFile, IconCode, IconCrossStroked,
  9. IconPaperclip, IconArrowUp, IconStop, IconClose, IconTemplateStroked,
  10. IconMusic, IconVideo, IconPdf, IconWord, IconExcel,
  11. IconSize
  12. } from '@douyinfe/semi-icons';
  13. import '@douyinfe/semi-foundation/aiChatInput/aiChatInput.scss';
  14. import HorizontalScroller from './horizontalScroller';
  15. import cls from 'classnames';
  16. import { getAttachmentType, isImageType, getContentType, getCustomSlotAttribute } from '@douyinfe/semi-foundation/aiChatInput/utils';
  17. import Configure from './configure';
  18. import RichTextInput from './richTextInput';
  19. import { Editor, FocusPosition } from '@tiptap/core';
  20. import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
  21. import { throttle } from 'lodash';
  22. import AIChatInputFoundation, { AIChatInputAdapter } from '@douyinfe/semi-foundation/aiChatInput/foundation';
  23. import { NodeSelection, TextSelection } from 'prosemirror-state';
  24. import { Node } from 'prosemirror-model';
  25. import ConfigContext, { ContextValue } from '../configProvider/context';
  26. import getConfigureItem from './configure/getConfigureItem';
  27. import { MessageContent } from '@douyinfe/semi-foundation/aiChatInput/interface';
  28. import { Content as TiptapContent } from "@tiptap/core";
  29. import { Locale } from '../locale/interface';
  30. import LocaleConsumer from '../locale/localeConsumer';
  31. import SkillItem from './skillItem';
  32. import SuggestionItem from './suggestionItem';
  33. export { getConfigureItem };
  34. export * from './interface';
  35. const prefixCls = cssClasses.PREFIX;
  36. class AIChatInput extends BaseComponent<AIChatInputProps, AIChatInputState> {
  37. static __SemiComponentName__ = "AIChatInput";
  38. static Configure = Configure;
  39. static contextType = ConfigContext;
  40. static getCustomSlotAttribute = getCustomSlotAttribute;
  41. private clickOutsideHandler: (e: Event) => void | null;
  42. static defaultProps: Partial<AIChatInputProps> = {
  43. onContentChange: noop,
  44. onStopGenerate: noop,
  45. showReference: true,
  46. showUploadFile: true,
  47. generating: false,
  48. dropdownMatchTriggerWidth: true,
  49. round: true,
  50. topSlotPosition: 'top',
  51. }
  52. constructor(props: AIChatInputProps) {
  53. super(props);
  54. this.editor = null;
  55. const defaultAttachment = props?.uploadProps?.defaultFileList ?? [];
  56. this.state = {
  57. popupKey: 1,
  58. templateVisible: false,
  59. skillVisible: false,
  60. suggestionVisible: false,
  61. attachments: defaultAttachment,
  62. content: null,
  63. popupWidth: null,
  64. skill: undefined,
  65. activeSkillIndex: 0,
  66. activeSuggestionIndex: 0,
  67. /**
  68. * richTextInit 用于标识富文本编辑区是否初始化完成,会影响初始化时发送按钮是否可以点击
  69. * richTextInit is used to identify whether the rich text editing area has been initialized,
  70. * which will affect whether the send button can be clicked during initialization.
  71. */
  72. richTextInit: false,
  73. };
  74. this.triggerRef = React.createRef();
  75. this.popUpOptionListID = getUuidShort();
  76. this.foundation = new AIChatInputFoundation(this.adapter);
  77. this.transformedContent = [];
  78. this.uploadRef = React.createRef();
  79. this.configureRef = React.createRef();
  80. this.richTextDIVRef = React.createRef<HTMLDivElement>();
  81. this.suggestionPanelRef = React.createRef<HTMLDivElement>();
  82. this.clickOutsideHandler = null;
  83. }
  84. editor: Editor;
  85. triggerRef: React.RefObject<HTMLDivElement>;
  86. configureRef: React.RefObject<Configure>;
  87. popUpOptionListID: string;
  88. foundation: AIChatInputFoundation;
  89. transformedContent: Content[];
  90. context: ContextValue;
  91. uploadRef: React.RefObject<Upload>;
  92. richTextDIVRef = React.createRef<HTMLDivElement>();
  93. suggestionPanelRef = React.createRef<HTMLDivElement>();
  94. get adapter(): AIChatInputAdapter<AIChatInputProps, AIChatInputState> {
  95. return {
  96. ...super.adapter,
  97. reposPopover: throttle(() => {
  98. const { templateVisible } = this.state;
  99. if (templateVisible) {
  100. this.setState({
  101. popupKey: this.state.popupKey + 1,
  102. });
  103. }
  104. }, 200),
  105. setContent: (content: string) => {
  106. this.editor.commands.setContent(content);
  107. },
  108. clearContent: () => {
  109. this.setContent('');
  110. },
  111. clearAttachments: () => {
  112. this.setState({
  113. attachments: [],
  114. });
  115. },
  116. focusEditor: (pos?: FocusPosition) => {
  117. this.editor?.commands.focus(pos || 'end');
  118. },
  119. getTriggerWidth: () => {
  120. const el = this.triggerRef.current;
  121. return el && el.getBoundingClientRect().width;
  122. },
  123. getEditor: () => this.editor,
  124. getPopupID: () => this.popUpOptionListID,
  125. notifySkillChange: (skill: Skill) => {
  126. this.props.onSkillChange?.(skill);
  127. },
  128. notifyContentChange: (result: Content[]) => {
  129. this.transformedContent = result;
  130. this.props.onContentChange?.(result);
  131. },
  132. notifyConfigureChange: (value: LeftMenuChangeProps, changedValue: LeftMenuChangeProps) => {
  133. this.props.onConfigureChange?.(value, changedValue);
  134. },
  135. manualUpload: (files: File[]) => {
  136. const uploadComponent = this.uploadRef.current;
  137. if (uploadComponent) {
  138. uploadComponent.insert(files);
  139. }
  140. },
  141. notifyMessageSend: (props: MessageContent) => {
  142. this.props.onMessageSend?.(props);
  143. },
  144. notifyStopGenerate: () => {
  145. this.props.onStopGenerate?.();
  146. },
  147. getRichTextDiv: () => this.richTextDIVRef?.current,
  148. registerClickOutsideHandler: cb => {
  149. const clickOutsideHandler = (e: Event) => {
  150. const optionsDom = this.suggestionPanelRef && this.suggestionPanelRef.current;
  151. const triggerDom = this.triggerRef && this.triggerRef.current;
  152. const target = e.target as Element;
  153. const path = e.composedPath && e.composedPath() || [target];
  154. if (
  155. optionsDom &&
  156. (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) &&
  157. triggerDom &&
  158. !triggerDom.contains(target) &&
  159. !(path.includes(triggerDom) || path.includes(optionsDom))
  160. ) {
  161. cb(e);
  162. }
  163. };
  164. this.clickOutsideHandler = clickOutsideHandler;
  165. document.addEventListener('mousedown', clickOutsideHandler, false);
  166. },
  167. unregisterClickOutsideHandler: () => {
  168. if (this.clickOutsideHandler) {
  169. document.removeEventListener('mousedown', this.clickOutsideHandler, false);
  170. }
  171. },
  172. handleReferenceDelete: (reference: Reference) => {
  173. this.props.onReferenceDelete?.(reference);
  174. },
  175. handleReferenceClick: (reference: Reference) => {
  176. this.props.onReferenceClick?.(reference);
  177. },
  178. isSelectionText: (selection: Selection) => {
  179. return selection instanceof TextSelection;
  180. },
  181. createSelection: (node: Node, pos: number) => {
  182. return NodeSelection.create(node, pos);
  183. },
  184. notifyFocus: (event: any) => {
  185. this.props.onFocus?.(event);
  186. },
  187. notifyBlur: (event: any) => {
  188. this.props.onBlur?.(event);
  189. },
  190. getConfigureValue: () => {
  191. return this.configureRef?.current?.getConfigureValue();
  192. }
  193. };
  194. }
  195. componentDidUpdate(prevProps: Readonly<AIChatInputProps>): void {
  196. const { suggestions } = this.props;
  197. if (!isEqual(suggestions, prevProps.suggestions)) {
  198. const newVisible = (suggestions && suggestions.length > 0) ? true : false;
  199. newVisible ? this.foundation.showSuggestionPanel() :
  200. this.foundation.hideSuggestionPanel();
  201. }
  202. if (this.props.generating && (this.props.generating !== prevProps.generating)) {
  203. this.adapter.clearContent();
  204. this.adapter.clearAttachments();
  205. }
  206. }
  207. componentWillUnmount(): void {
  208. this.foundation.destroy();
  209. }
  210. // ref method
  211. setContent = (content: TiptapContent) => {
  212. this.adapter.setContent(content);
  213. };
  214. // ref method
  215. focusEditor = (pos: FocusPosition) => {
  216. this.adapter.focusEditor(pos);
  217. }
  218. // ref method & inner method
  219. changeTemplateVisible = (value: boolean) => {
  220. this.foundation.changeTemplateVisible(value);
  221. }
  222. // ref method & inner method
  223. getEditor = () => this.editor;
  224. // ref method
  225. deleteContent(content: Content) {
  226. this.foundation.handleDeleteContent(content);
  227. }
  228. setEditor = (editor: Editor) => {
  229. this.editor = editor;
  230. }
  231. setContentWhileSaveTool = (content: string) => {
  232. const { skill } = this.state;
  233. let realContent = '';
  234. if (!skill) {
  235. realContent = `<p>${content}</p>`;
  236. } else {
  237. realContent = `<p><skill-slot data-value=${skill.label ?? 'test'}></skill-slot>${content}</p>`;
  238. }
  239. this.setContent(realContent);
  240. }
  241. renderTemplate() {
  242. const { skill } = this.state;
  243. const { renderTemplate, templatesStyle, templatesCls } = this.props;
  244. const { popupWidth } = this.state;
  245. return <div
  246. className={cls(`${prefixCls}-template`, {
  247. [templatesCls]: templatesCls,
  248. })}
  249. style={{ width: popupWidth, maxHeight: 500, ...templatesStyle }}
  250. >
  251. {renderTemplate?.(skill, this.setContent)}
  252. </div>;
  253. }
  254. renderSkill() {
  255. const { popupWidth } = this.state;
  256. const { skills, renderSkillItem } = this.props;
  257. return <div
  258. id={`${prefixCls}-skill-${this.popUpOptionListID}`}
  259. className={`${prefixCls}-skill`}
  260. style={{ width: popupWidth, maxHeight: numbers.SKILL_MAX_HEIGHT }}
  261. >
  262. {
  263. skills?.map((item, index) => (<SkillItem
  264. index={index}
  265. isActive={this.state.activeSkillIndex === index}
  266. key={item.key || item.value}
  267. skill={item}
  268. renderSkillItem={renderSkillItem}
  269. onClick={this.foundation.handleSkillSelect}
  270. onMouseEnter={this.foundation.setActiveSkillIndex}
  271. />))
  272. }
  273. </div>;
  274. }
  275. renderSuggestions() {
  276. const { suggestions, renderSuggestionItem } = this.props;
  277. const { popupWidth, activeSuggestionIndex } = this.state;
  278. return (<div
  279. id={`${prefixCls}-suggestion-${this.popUpOptionListID}`}
  280. className={`${prefixCls}-suggestion`}
  281. style={{ width: popupWidth, maxHeight: numbers.SUGGESTION_MAX_HEIGHT }}
  282. ref={this.suggestionPanelRef}
  283. >
  284. {
  285. suggestions.map((item, index) => (
  286. <SuggestionItem
  287. index={index}
  288. key={typeof item === 'string' ? item : (item && 'content' in item ? item.content : index)}
  289. suggestion={item}
  290. isActive={activeSuggestionIndex === index}
  291. renderSuggestionItem={renderSuggestionItem}
  292. onClick={this.foundation.handleSuggestionSelect}
  293. onMouseEnter={this.foundation.setActiveSuggestionIndex}
  294. />
  295. ))
  296. }
  297. </div>
  298. );
  299. }
  300. renderPopoverContent() {
  301. const { templateVisible, skillVisible, suggestionVisible } = this.state;
  302. if (templateVisible) {
  303. return this.renderTemplate();
  304. } else if (skillVisible) {
  305. return this.renderSkill();
  306. } else if (suggestionVisible) {
  307. return this.renderSuggestions();
  308. } else {
  309. return null;
  310. }
  311. }
  312. handleReferenceDelete = (reference: Reference) => {
  313. const { onReferenceDelete } = this.props;
  314. onReferenceDelete(reference);
  315. }
  316. getIconByType(type: string, size: IconSize = 'small') {
  317. let iconNode: React.ReactNode;
  318. if (type === 'text') {
  319. return null;
  320. }
  321. switch (type) {
  322. case 'file':
  323. case 'word':
  324. iconNode = <IconWord size={size} />;
  325. break;
  326. case 'code':
  327. iconNode = <IconCode size={size} />;
  328. break;
  329. case 'excel':
  330. iconNode = <IconExcel size={size} />;
  331. break;
  332. case 'video':
  333. iconNode = <IconVideo size={size} />;
  334. break;
  335. case 'audio':
  336. iconNode = <IconMusic size={size} />;
  337. break;
  338. case 'pdf':
  339. iconNode = <IconPdf size={size} />;
  340. break;
  341. default:
  342. iconNode = <IconFile size={size} />;
  343. break;
  344. }
  345. return iconNode;
  346. }
  347. getReferenceIconByType(type: string) {
  348. let iconNode = this.getIconByType(type);
  349. return <span className={`${prefixCls}-ref-icon ${prefixCls}-ref-icon-${type} ${prefixCls}-reference-icon`}>
  350. {iconNode}
  351. </span>;
  352. }
  353. getAttachmentIconByType(type: string) {
  354. let iconNode = this.getIconByType(type, 'large');
  355. return <span className={`${prefixCls}-attachment-icon ${prefixCls}-ref-icon ${prefixCls}-ref-icon-${type}`}>
  356. {iconNode}
  357. </span>;
  358. }
  359. renderReference() {
  360. const { references = [], renderReference } = this.props;
  361. if (references.length === 0 ) {
  362. return null;
  363. }
  364. return <div className={`${prefixCls}-references`}>
  365. {references.map(item => {
  366. if (renderReference) {
  367. return renderReference(item);
  368. }
  369. const { id, type, content, name, url } = item;
  370. const isImage = isImageType(item);
  371. const signIconType = getContentType(getAttachmentType(item));
  372. // eslint-disable-next-line jsx-a11y/click-events-have-key-events
  373. return <div
  374. key={id}
  375. className={`${prefixCls}-reference`}
  376. onClick={() => { this.foundation.handleReferenceClick(item);}}
  377. >
  378. <IconSendMsgStroked />
  379. <span className={`${prefixCls}-reference-content`}>
  380. {type !== 'text' && ( isImage ? <img className={`${prefixCls}-reference-img`} src={url} alt={name}></img> :
  381. this.getReferenceIconByType(signIconType))}
  382. <span className={`${prefixCls}-reference-name`}>{type === 'text' ? content : name}</span>
  383. </span>
  384. <IconCrossStroked
  385. size="small"
  386. className={`${prefixCls}-reference-delete`}
  387. onClick={(e) => {
  388. this.handleReferenceDelete(item);
  389. e.stopPropagation();
  390. }}
  391. />
  392. </div>;
  393. })}
  394. </div>;
  395. }
  396. // ref method
  397. deleteUploadFile = (item: Attachment) => {
  398. this.foundation.handleUploadFileDelete(item);
  399. }
  400. renderAttachment() {
  401. const { attachments = [] } = this.state;
  402. if (attachments.length === 0) {
  403. return null;
  404. }
  405. return <HorizontalScroller prefix={`${prefixCls}`}>
  406. {attachments?.map((item: Attachment, index: number) => {
  407. const isImage = isImageType(item);
  408. const realType = getAttachmentType(item);
  409. const signIconType = getContentType(realType);
  410. const { uid, name, url, size, percent, status } = item;
  411. const showPercent = !(percent === 100 || typeof percent === 'undefined') && status === 'uploading';
  412. return <div className={`${prefixCls}-attachment`} key={uid}>
  413. {isImage ? <img className={`${prefixCls}-attachment-img`} src={url} alt={name}></img>
  414. : this.getAttachmentIconByType(signIconType)
  415. }
  416. <div className={`${prefixCls}-attachment-content`}>
  417. <div className={`${prefixCls}-attachment-content-name`}>{name}</div>
  418. <div className={`${prefixCls}-attachment-content-size`}>{`${realType} ${size}`}</div>
  419. </div>
  420. {showPercent && <Progress
  421. type="circle"
  422. width={30}
  423. className={`${prefixCls}-attachment-progress`}
  424. percent={percent}
  425. showInfo={false}
  426. aria-label="upload progress"
  427. />}
  428. <IconClose
  429. className={`${prefixCls}-attachment-delete`}
  430. size="small"
  431. onClick={() => { this.foundation.handleUploadFileDelete(item);}}
  432. />
  433. </div>;
  434. }
  435. )}
  436. </HorizontalScroller>;
  437. }
  438. renderTopArea() {
  439. const { references, topSlotPosition, renderTopSlot, showReference, showUploadFile } = this.props;
  440. const { attachments } = this.state;
  441. const topSlot = renderTopSlot?.({
  442. references,
  443. attachments,
  444. content: this.transformedContent,
  445. handleUploadFileDelete: this.foundation.handleUploadFileDelete,
  446. handleReferenceDelete: this.handleReferenceDelete,
  447. });
  448. return <>
  449. {topSlotPosition === 'top' && topSlot}
  450. {showReference && this.renderReference()}
  451. {topSlotPosition === 'middle' && topSlot}
  452. {showUploadFile && this.renderAttachment()}
  453. {topSlotPosition === 'bottom' && topSlot}
  454. </>;
  455. }
  456. renderLeftFooter = () => {
  457. const { renderConfigureArea, round, showTemplateButton } = this.props;
  458. const { skill = {} } = this.state;
  459. const { hasTemplate } = skill as Skill;
  460. return <LocaleConsumer componentName="AIChatInput">
  461. {(locale: Locale['AIChatInput']) => (
  462. <div className={`${prefixCls}-footer-configure`}>
  463. <Configure
  464. ref={this.configureRef}
  465. round={round}
  466. onChange={this.foundation.onConfigureChange}
  467. >
  468. {renderConfigureArea?.()}
  469. {(showTemplateButton || hasTemplate) && <Configure.Button
  470. key={"template"}
  471. field="template"
  472. onClick={this.changeTemplateVisible}
  473. icon={<IconTemplateStroked />}
  474. >{locale.template}</Configure.Button>}
  475. </Configure>
  476. </div>)}
  477. </LocaleConsumer>;
  478. }
  479. renderUploadButton = () => {
  480. const { uploadTipProps, uploadProps } = this.props;
  481. const { attachments } = this.state;
  482. const { className, onChange, renderFileItem, children, ...rest } = uploadProps ?? {};
  483. const realUploadProps = {
  484. ...rest,
  485. onChange: this.foundation.onUploadChange,
  486. };
  487. const uploadNode = <Upload
  488. ref={this.uploadRef}
  489. fileList={attachments}
  490. listType="none"
  491. {...realUploadProps}
  492. key='upload'
  493. >
  494. <button className={`${prefixCls}-footer-action-button ${prefixCls}-footer-action-upload`} >
  495. <IconPaperclip />
  496. </button>
  497. </Upload>;
  498. return uploadTipProps ? <Tooltip {...uploadTipProps} key='upload'><span>{uploadNode}</span></Tooltip> : uploadNode;
  499. }
  500. renderSendButton = () => {
  501. const { generating } = this.props;
  502. const canSend = this.foundation.canSend();
  503. return <button
  504. key="send"
  505. className={cls(`${prefixCls}-footer-action-button`, {
  506. [`${prefixCls}-footer-action-send`]: !generating,
  507. [`${prefixCls}-footer-action-stop`]: generating,
  508. [`${prefixCls}-footer-action-send-disabled`]: !generating && !canSend,
  509. })}
  510. onClick={this.foundation.handleSend}
  511. >
  512. {generating ? <IconStop /> : <IconArrowUp />}
  513. </button>;
  514. }
  515. renderRightFooter = () => {
  516. const { renderActionArea } = this.props;
  517. const actionCls = `${prefixCls}-footer-action`;
  518. const actionNode = [
  519. this.renderUploadButton(),
  520. this.renderSendButton(),
  521. ];
  522. if (renderActionArea) {
  523. return renderActionArea({
  524. menuItem: actionNode,
  525. className: actionCls
  526. });
  527. }
  528. return <div className={actionCls}>
  529. {actionNode}
  530. </div>;
  531. }
  532. renderFooter = () => {
  533. const round = this.props.round;
  534. return <div className={cls(`${prefixCls}-footer`, { [`${prefixCls}-footer-round`]: round })}>
  535. {this.renderLeftFooter()}
  536. {this.renderRightFooter()}
  537. </div>;
  538. }
  539. render() {
  540. const { direction } = this.context;
  541. const defaultPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  542. const { style, className, popoverProps, placeholder, extensions, defaultContent } = this.props;
  543. const { templateVisible, skillVisible, suggestionVisible, popupKey } = this.state;
  544. return (
  545. <Popover
  546. position={defaultPosition}
  547. {...popoverProps}
  548. rePosKey={popupKey}
  549. className={cls({
  550. [`${prefixCls}-popover-suggestion`]: suggestionVisible,
  551. [`${prefixCls}-popover-skill`]: skillVisible,
  552. [`${prefixCls}-popover-template`]: templateVisible,
  553. })}
  554. content={this.renderPopoverContent()}
  555. visible={templateVisible || skillVisible || suggestionVisible}
  556. trigger="custom"
  557. disableArrowKeyDown={true}
  558. >
  559. {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
  560. <div
  561. className={cls(prefixCls, { [className]: className })}
  562. style={style}
  563. ref={this.triggerRef}
  564. onClick={this.foundation.handleContainerClick}
  565. onMouseDown={this.foundation.handleContainerMouseDown}
  566. >
  567. {this.renderTopArea()}
  568. <RichTextInput
  569. innerRef={this.richTextDIVRef}
  570. defaultContent={defaultContent}
  571. placeholder={placeholder}
  572. onKeyDown={this.foundation.handleKeyDown}
  573. setEditor={this.setEditor}
  574. onChange={this.foundation.handleContentChange}
  575. extensions={extensions}
  576. handleKeyDown={this.foundation.handRichTextArealKeyDown}
  577. onPaste={this.foundation.handlePaste}
  578. onFocus={this.foundation.handleFocus}
  579. onBlur={this.foundation.handleBlur}
  580. handleCreate={this.foundation.handleCreate}
  581. />
  582. {this.renderFooter()}
  583. </div>
  584. </Popover>
  585. );
  586. }
  587. }
  588. export default AIChatInput;