@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.
699 lines • 25.5 kB
JavaScript
import _throttle from "lodash/throttle";
import _isEqual from "lodash/isEqual";
import _noop from "lodash/noop";
var __rest = this && this.__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React from 'react';
import BaseComponent from '../_base/baseComponent';
import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/lib/es/aiChatInput/constants';
import { Popover, Tooltip, Upload, Progress } from '../index';
import { IconSendMsgStroked, IconFile, IconCode, IconCrossStroked, IconPaperclip, IconArrowUp, IconStop, IconClose, IconTemplateStroked, IconMusic, IconVideo, IconPdf, IconWord, IconExcel } from '@douyinfe/semi-icons';
import '@douyinfe/semi-foundation/lib/es/aiChatInput/aiChatInput.css';
import HorizontalScroller from './horizontalScroller';
import cls from 'classnames';
import { getAttachmentType, isImageType, getContentType, getCustomSlotAttribute, getSkillSlotString } from '@douyinfe/semi-foundation/lib/es/aiChatInput/utils';
import Configure from './configure';
import RichTextInput from './richTextInput';
import { getUuidShort } from '@douyinfe/semi-foundation/lib/es/utils/uuid';
import AIChatInputFoundation from '@douyinfe/semi-foundation/lib/es/aiChatInput/foundation';
import { NodeSelection, TextSelection } from 'prosemirror-state';
import ConfigContext from '../configProvider/context';
import getConfigureItem from './configure/getConfigureItem';
import LocaleConsumer from '../locale/localeConsumer';
import SkillItem from './skillItem';
import SuggestionItem from './suggestionItem';
export { getConfigureItem };
export * from './interface';
const prefixCls = cssClasses.PREFIX;
class AIChatInput extends BaseComponent {
constructor(props) {
var _a, _b;
super(props);
this.richTextDIVRef = /*#__PURE__*/React.createRef();
this.suggestionPanelRef = /*#__PURE__*/React.createRef();
// ref method
this.setContent = content => {
this.adapter.setContent(content);
};
// ref method
this.focusEditor = pos => {
this.adapter.focusEditor(pos);
};
// ref method & inner method
this.changeTemplateVisible = value => {
this.foundation.changeTemplateVisible(value);
};
// ref method & inner method
this.getEditor = () => this.editor;
this.setEditor = editor => {
this.editor = editor;
};
this.setContentWhileSaveTool = content => {
const {
skill
} = this.state;
let realContent = '';
if (!skill) {
realContent = `<p>${content}</p>`;
} else {
realContent = `<p>${getSkillSlotString(skill)}${content}</p>`;
}
this.setContent(realContent);
};
this.handleReferenceDelete = reference => {
const {
onReferenceDelete
} = this.props;
onReferenceDelete(reference);
};
// ref method
this.deleteUploadFile = item => {
this.foundation.handleUploadFileDelete(item);
};
this.renderLeftFooter = () => {
const {
renderConfigureArea,
round,
showTemplateButton
} = this.props;
const {
skill = {}
} = this.state;
const {
hasTemplate
} = skill;
return /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "AIChatInput"
}, locale => (/*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-footer-configure`
}, /*#__PURE__*/React.createElement(Configure, {
ref: this.configureRef,
round: round,
onChange: this.foundation.onConfigureChange
}, renderConfigureArea === null || renderConfigureArea === void 0 ? void 0 : renderConfigureArea(), (showTemplateButton || hasTemplate) && /*#__PURE__*/React.createElement(Configure.Button, {
key: "template",
field: "template",
onClick: this.changeTemplateVisible,
icon: /*#__PURE__*/React.createElement(IconTemplateStroked, null)
}, locale.template)))));
};
this.renderUploadNode = () => {
const {
uploadTipProps,
uploadProps,
renderUploadButton
} = this.props;
const {
attachments
} = this.state;
const _a = uploadProps !== null && uploadProps !== void 0 ? uploadProps : {},
{
children
} = _a,
rest = __rest(_a, ["children"]);
const realUploadProps = Object.assign(Object.assign({}, rest), {
onChange: this.foundation.onUploadChange
});
const defaultButtonNode = /*#__PURE__*/React.createElement("button", {
className: `${prefixCls}-footer-action-button ${prefixCls}-footer-action-upload`
}, /*#__PURE__*/React.createElement(IconPaperclip, null));
const openFileDialog = () => {
var _a, _b;
(_b = (_a = this.uploadRef.current) === null || _a === void 0 ? void 0 : _a.openFileDialog) === null || _b === void 0 ? void 0 : _b.call(_a);
};
const renderProps = {
defaultNode: children !== null && children !== void 0 ? children : defaultButtonNode,
openFileDialog,
disabled: Boolean(uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.disabled),
attachments: attachments !== null && attachments !== void 0 ? attachments : []
};
const uploadChild = renderUploadButton ? renderUploadButton(renderProps) : renderProps.defaultNode;
const uploadNode = /*#__PURE__*/React.createElement(Upload, Object.assign({
ref: this.uploadRef,
fileList: attachments,
listType: "none"
}, realUploadProps, {
key: 'upload'
}), uploadChild);
return uploadTipProps ? /*#__PURE__*/React.createElement(Tooltip, Object.assign({}, uploadTipProps, {
key: 'upload'
}), /*#__PURE__*/React.createElement("span", null, uploadNode)) : uploadNode;
};
this.renderSendButton = () => {
const {
generating
} = this.props;
const canSend = this.foundation.canSend();
return /*#__PURE__*/React.createElement("button", {
key: "send",
className: cls(`${prefixCls}-footer-action-button`, {
[`${prefixCls}-footer-action-send`]: !generating,
[`${prefixCls}-footer-action-stop`]: generating,
[`${prefixCls}-footer-action-send-disabled`]: !generating && !canSend
}),
onClick: generating ? this.foundation.handleStopGenerate : this.foundation.handleSend
}, generating ? /*#__PURE__*/React.createElement(IconStop, null) : /*#__PURE__*/React.createElement(IconArrowUp, null));
};
this.renderRightFooter = () => {
const {
renderActionArea,
showUploadButton
} = this.props;
const actionCls = `${prefixCls}-footer-action`;
const actionNode = [showUploadButton && this.renderUploadNode(), this.renderSendButton()].filter(Boolean);
if (renderActionArea) {
return renderActionArea({
menuItem: actionNode,
className: actionCls
});
}
return /*#__PURE__*/React.createElement("div", {
className: actionCls
}, actionNode);
};
this.renderFooter = () => {
const round = this.props.round;
return /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-footer`, {
[`${prefixCls}-footer-round`]: round
})
}, this.renderLeftFooter(), this.renderRightFooter());
};
this.editor = null;
const defaultAttachment = (_b = (_a = props === null || props === void 0 ? void 0 : props.uploadProps) === null || _a === void 0 ? void 0 : _a.defaultFileList) !== null && _b !== void 0 ? _b : [];
this.state = {
popupKey: 1,
templateVisible: false,
skillVisible: false,
suggestionVisible: false,
attachments: defaultAttachment,
content: null,
popupWidth: null,
skill: undefined,
activeSkillIndex: 0,
activeSuggestionIndex: 0,
/**
* richTextInit 用于标识富文本编辑区是否初始化完成,会影响初始化时发送按钮是否可以点击
* richTextInit is used to identify whether the rich text editing area has been initialized,
* which will affect whether the send button can be clicked during initialization.
*/
richTextInit: false
};
this.triggerRef = /*#__PURE__*/React.createRef();
this.popUpOptionListID = getUuidShort();
this.foundation = new AIChatInputFoundation(this.adapter);
this.transformedContent = [];
this.uploadRef = /*#__PURE__*/React.createRef();
this.configureRef = /*#__PURE__*/React.createRef();
this.richTextDIVRef = /*#__PURE__*/React.createRef();
this.suggestionPanelRef = /*#__PURE__*/React.createRef();
this.clickOutsideHandler = null;
}
get adapter() {
return Object.assign(Object.assign({}, super.adapter), {
reposPopover: _throttle(() => {
const {
templateVisible
} = this.state;
if (templateVisible) {
this.setState({
popupKey: this.state.popupKey + 1
});
}
}, 200),
setContent: content => {
this.editor.commands.setContent(content);
},
clearContent: () => {
this.setContent('');
},
clearAttachments: () => {
this.setState({
attachments: []
});
},
focusEditor: pos => {
var _a;
(_a = this.editor) === null || _a === void 0 ? void 0 : _a.commands.focus(pos || 'end');
},
getTriggerWidth: () => {
const el = this.triggerRef.current;
return el && el.getBoundingClientRect().width;
},
getEditor: () => this.editor,
getPopupID: () => this.popUpOptionListID,
notifySkillChange: skill => {
var _a, _b;
(_b = (_a = this.props).onSkillChange) === null || _b === void 0 ? void 0 : _b.call(_a, skill);
},
notifyContentChange: result => {
var _a, _b;
this.transformedContent = result;
(_b = (_a = this.props).onContentChange) === null || _b === void 0 ? void 0 : _b.call(_a, result);
},
notifyConfigureChange: (value, changedValue) => {
var _a, _b;
(_b = (_a = this.props).onConfigureChange) === null || _b === void 0 ? void 0 : _b.call(_a, value, changedValue);
},
manualUpload: files => {
const uploadComponent = this.uploadRef.current;
if (uploadComponent) {
uploadComponent.insert(files);
}
},
notifyMessageSend: props => {
var _a, _b;
(_b = (_a = this.props).onMessageSend) === null || _b === void 0 ? void 0 : _b.call(_a, props);
},
notifyStopGenerate: () => {
var _a, _b;
(_b = (_a = this.props).onStopGenerate) === null || _b === void 0 ? void 0 : _b.call(_a);
},
getRichTextDiv: () => {
var _a;
return (_a = this.richTextDIVRef) === null || _a === void 0 ? void 0 : _a.current;
},
registerClickOutsideHandler: cb => {
const clickOutsideHandler = e => {
const optionsDom = this.suggestionPanelRef && this.suggestionPanelRef.current;
const triggerDom = this.triggerRef && this.triggerRef.current;
const target = e.target;
const path = e.composedPath && e.composedPath() || [target];
if (optionsDom && (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) && triggerDom && !triggerDom.contains(target) && !(path.includes(triggerDom) || path.includes(optionsDom))) {
cb(e);
}
};
this.clickOutsideHandler = clickOutsideHandler;
document.addEventListener('mousedown', clickOutsideHandler, false);
},
unregisterClickOutsideHandler: () => {
if (this.clickOutsideHandler) {
document.removeEventListener('mousedown', this.clickOutsideHandler, false);
}
},
handleReferenceDelete: reference => {
var _a, _b;
(_b = (_a = this.props).onReferenceDelete) === null || _b === void 0 ? void 0 : _b.call(_a, reference);
},
handleReferenceClick: reference => {
var _a, _b;
(_b = (_a = this.props).onReferenceClick) === null || _b === void 0 ? void 0 : _b.call(_a, reference);
},
isSelectionText: selection => {
return selection instanceof TextSelection;
},
createSelection: (node, pos) => {
return NodeSelection.create(node, pos);
},
notifyFocus: event => {
var _a, _b;
(_b = (_a = this.props).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, event);
},
notifyBlur: event => {
var _a, _b;
(_b = (_a = this.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, event);
},
getConfigureValue: () => {
var _a, _b;
return (_b = (_a = this.configureRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.getConfigureValue();
}
});
}
componentDidUpdate(prevProps) {
const {
suggestions,
keepSkillAfterSend,
clearContentOnGenerating
} = this.props;
if (!_isEqual(suggestions, prevProps.suggestions)) {
const newVisible = suggestions && suggestions.length > 0 ? true : false;
newVisible ? this.foundation.showSuggestionPanel() : this.foundation.hideSuggestionPanel();
}
if (this.props.generating && this.props.generating !== prevProps.generating) {
if (clearContentOnGenerating !== false) {
keepSkillAfterSend ? this.setContentWhileSaveTool('') : this.adapter.clearContent();
this.adapter.clearAttachments();
}
}
}
componentWillUnmount() {
this.foundation.destroy();
}
// ref method
deleteContent(content) {
this.foundation.handleDeleteContent(content);
}
renderTemplate() {
const {
skill
} = this.state;
const {
renderTemplate,
templatesStyle,
templatesCls
} = this.props;
const {
popupWidth
} = this.state;
return /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-template`, {
[templatesCls]: templatesCls
}),
style: Object.assign({
width: popupWidth,
maxHeight: 500
}, templatesStyle)
}, renderTemplate === null || renderTemplate === void 0 ? void 0 : renderTemplate(skill, this.setContent));
}
renderSkill() {
const {
popupWidth
} = this.state;
const {
skills,
renderSkillItem
} = this.props;
return /*#__PURE__*/React.createElement("div", {
id: `${prefixCls}-skill-${this.popUpOptionListID}`,
className: `${prefixCls}-skill`,
style: {
width: popupWidth,
maxHeight: numbers.SKILL_MAX_HEIGHT
}
}, skills === null || skills === void 0 ? void 0 : skills.map((item, index) => (/*#__PURE__*/React.createElement(SkillItem, {
index: index,
isActive: this.state.activeSkillIndex === index,
key: item.key || item.value,
skill: item,
renderSkillItem: renderSkillItem,
onClick: this.foundation.handleSkillSelect,
onMouseEnter: this.foundation.setActiveSkillIndex
}))));
}
renderSuggestions() {
const {
suggestions,
renderSuggestionItem
} = this.props;
const {
popupWidth,
activeSuggestionIndex
} = this.state;
return /*#__PURE__*/React.createElement("div", {
id: `${prefixCls}-suggestion-${this.popUpOptionListID}`,
className: `${prefixCls}-suggestion`,
style: {
width: popupWidth,
maxHeight: numbers.SUGGESTION_MAX_HEIGHT
},
ref: this.suggestionPanelRef
}, suggestions.map((item, index) => (/*#__PURE__*/React.createElement(SuggestionItem, {
index: index,
key: typeof item === 'string' ? item : item && 'content' in item ? item.content : index,
suggestion: item,
isActive: activeSuggestionIndex === index,
renderSuggestionItem: renderSuggestionItem,
onClick: this.foundation.handleSuggestionSelect,
onMouseEnter: this.foundation.setActiveSuggestionIndex
}))));
}
renderPopoverContent() {
const {
templateVisible,
skillVisible,
suggestionVisible
} = this.state;
if (templateVisible) {
return this.renderTemplate();
} else if (skillVisible) {
return this.renderSkill();
} else if (suggestionVisible) {
return this.renderSuggestions();
} else {
return null;
}
}
getIconByType(type) {
let size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'small';
let iconNode;
if (type === 'text') {
return null;
}
switch (type) {
case 'file':
case 'word':
iconNode = /*#__PURE__*/React.createElement(IconWord, {
size: size
});
break;
case 'code':
iconNode = /*#__PURE__*/React.createElement(IconCode, {
size: size
});
break;
case 'excel':
iconNode = /*#__PURE__*/React.createElement(IconExcel, {
size: size
});
break;
case 'video':
iconNode = /*#__PURE__*/React.createElement(IconVideo, {
size: size
});
break;
case 'audio':
iconNode = /*#__PURE__*/React.createElement(IconMusic, {
size: size
});
break;
case 'pdf':
iconNode = /*#__PURE__*/React.createElement(IconPdf, {
size: size
});
break;
default:
iconNode = /*#__PURE__*/React.createElement(IconFile, {
size: size
});
break;
}
return iconNode;
}
getReferenceIconByType(type) {
let iconNode = this.getIconByType(type);
return /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-ref-icon ${prefixCls}-ref-icon-${type} ${prefixCls}-reference-icon`
}, iconNode);
}
getAttachmentIconByType(type) {
let iconNode = this.getIconByType(type, 'large');
return /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-attachment-icon ${prefixCls}-ref-icon ${prefixCls}-ref-icon-${type}`
}, iconNode);
}
renderReference() {
const {
references = [],
renderReference
} = this.props;
if (references.length === 0) {
return null;
}
return /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-references`
}, references.map(item => {
if (renderReference) {
return renderReference(item);
}
const {
id,
type,
content,
name,
url
} = item;
const isImage = isImageType(item);
const signIconType = getContentType(getAttachmentType(item));
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
return /*#__PURE__*/React.createElement("div", {
key: id,
className: `${prefixCls}-reference`,
onClick: () => {
this.foundation.handleReferenceClick(item);
}
}, /*#__PURE__*/React.createElement(IconSendMsgStroked, null), /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-reference-content`
}, type !== 'text' && (isImage ? /*#__PURE__*/React.createElement("img", {
className: `${prefixCls}-reference-img`,
src: url,
alt: name
}) : this.getReferenceIconByType(signIconType)), /*#__PURE__*/React.createElement("span", {
className: `${prefixCls}-reference-name`
}, type === 'text' ? content : name)), /*#__PURE__*/React.createElement(IconCrossStroked, {
size: "small",
className: `${prefixCls}-reference-delete`,
onClick: e => {
this.handleReferenceDelete(item);
e.stopPropagation();
}
}));
}));
}
renderAttachment() {
const {
attachments = []
} = this.state;
if (attachments.length === 0) {
return null;
}
return /*#__PURE__*/React.createElement(HorizontalScroller, {
prefix: `${prefixCls}`
}, attachments === null || attachments === void 0 ? void 0 : attachments.map((item, index) => {
const isImage = isImageType(item);
const realType = getAttachmentType(item);
const signIconType = getContentType(realType);
const {
uid,
name,
url,
size,
percent,
status
} = item;
const showPercent = !(percent === 100 || typeof percent === 'undefined') && status === 'uploading';
return /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-attachment`,
key: uid
}, isImage ? /*#__PURE__*/React.createElement("img", {
className: `${prefixCls}-attachment-img`,
src: url,
alt: name
}) : this.getAttachmentIconByType(signIconType), /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-attachment-content`
}, /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-attachment-content-name`
}, name), /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-attachment-content-size`
}, `${realType} ${size}`)), showPercent && /*#__PURE__*/React.createElement(Progress, {
type: "circle",
width: 30,
className: `${prefixCls}-attachment-progress`,
percent: percent,
showInfo: false,
"aria-label": "upload progress"
}), /*#__PURE__*/React.createElement(IconClose, {
className: `${prefixCls}-attachment-delete`,
size: "small",
onClick: () => {
this.foundation.handleUploadFileDelete(item);
}
}));
}));
}
renderTopArea() {
const {
references,
topSlotPosition,
renderTopSlot,
showReference,
showUploadFile
} = this.props;
const {
attachments
} = this.state;
const topSlot = renderTopSlot === null || renderTopSlot === void 0 ? void 0 : renderTopSlot({
references,
attachments,
content: this.transformedContent,
handleUploadFileDelete: this.foundation.handleUploadFileDelete,
handleReferenceDelete: this.handleReferenceDelete
});
return /*#__PURE__*/React.createElement(React.Fragment, null, topSlotPosition === 'top' && topSlot, showReference && this.renderReference(), topSlotPosition === 'middle' && topSlot, showUploadFile && this.renderAttachment(), topSlotPosition === 'bottom' && topSlot);
}
render() {
const {
direction
} = this.context;
const defaultPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
const {
style,
className,
popoverProps,
placeholder,
extensions,
defaultContent,
immediatelyRender,
onPaste
} = this.props;
const {
templateVisible,
skillVisible,
suggestionVisible,
popupKey
} = this.state;
return /*#__PURE__*/React.createElement(Popover, Object.assign({
position: defaultPosition
}, popoverProps, {
rePosKey: popupKey,
className: cls({
[`${prefixCls}-popover-suggestion`]: suggestionVisible,
[`${prefixCls}-popover-skill`]: skillVisible,
[`${prefixCls}-popover-template`]: templateVisible
}),
content: this.renderPopoverContent(),
visible: templateVisible || skillVisible || suggestionVisible,
trigger: "custom",
disableArrowKeyDown: true
}), /*#__PURE__*/React.createElement("div", {
className: cls(prefixCls, {
[className]: className
}),
style: style,
ref: this.triggerRef,
onClick: this.foundation.handleContainerClick,
onMouseDown: this.foundation.handleContainerMouseDown
}, this.renderTopArea(), /*#__PURE__*/React.createElement(RichTextInput, {
immediatelyRender: immediatelyRender,
innerRef: this.richTextDIVRef,
defaultContent: defaultContent,
placeholder: placeholder,
onKeyDown: this.foundation.handleKeyDown,
setEditor: this.setEditor,
onChange: this.foundation.handleContentChange,
extensions: extensions,
handleKeyDown: this.foundation.handRichTextArealKeyDown,
onPasteEvent: onPaste,
onPaste: this.foundation.handlePaste,
onFocus: this.foundation.handleFocus,
onBlur: this.foundation.handleBlur,
handleCreate: this.foundation.handleCreate,
showPlaceholderWhenSkillOnly: this.props.showPlaceholderWhenSkillOnly
}), this.renderFooter()));
}
}
AIChatInput.__SemiComponentName__ = "AIChatInput";
AIChatInput.Configure = Configure;
AIChatInput.contextType = ConfigContext;
AIChatInput.getCustomSlotAttribute = getCustomSlotAttribute;
AIChatInput.defaultProps = {
onContentChange: _noop,
onStopGenerate: _noop,
showReference: true,
showUploadFile: true,
generating: false,
dropdownMatchTriggerWidth: true,
round: true,
topSlotPosition: 'top',
sendHotKey: strings.SEND_HOTKEY.ENTER,
keepSkillAfterSend: false,
showUploadButton: true,
clearContentOnGenerating: true
};
export default AIChatInput;