@ant-design/x
Version:
Craft AI-driven interfaces effortlessly
189 lines (184 loc) • 7.46 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import pickAttrs from '@rc-component/util/lib/pickAttrs';
import { clsx } from 'clsx';
import React from 'react';
import useXComponentConfig from "../_util/hooks/use-x-component-config";
import { useXProviderContext } from "../x-provider";
import { BubbleContext } from "./context";
import { EditableContent } from "./EditableContent";
import Loading from "./loading";
import useBubbleStyle from "./style";
import { TypingContent } from "./TypingContent";
const Bubble = ({
prefixCls: customizePrefixCls,
rootClassName,
style,
className,
styles = {},
classNames = {},
placement = 'start',
content,
contentRender,
editable = false,
typing,
streaming = false,
variant = 'filled',
shape = 'default',
header,
footer,
avatar,
extra,
footerPlacement,
loading,
loadingRender,
onTyping,
onTypingComplete,
onEditConfirm,
onEditCancel,
...restProps
}, ref) => {
// ======================== Ref ==========================
const rootDiv = React.useRef(null);
React.useImperativeHandle(ref, () => ({
nativeElement: rootDiv.current
}));
// ===================== Component Config =========================
const contextConfig = useXComponentConfig('bubble');
// ============================ Prefix ============================
const {
direction,
getPrefixCls
} = useXProviderContext();
const prefixCls = getPrefixCls('bubble', customizePrefixCls);
// ============================= Bubble context ==============================
const context = React.useContext(BubbleContext);
// ============================ Styles ============================
const [hashId, cssVarCls] = useBubbleStyle(prefixCls);
const rootMergedStyle = {
...contextConfig.style,
...contextConfig.styles.root,
...styles.root,
...style
};
const rootMergedCls = clsx(prefixCls, contextConfig.className, contextConfig.classNames.root, classNames.root, rootClassName, className, hashId, cssVarCls, `${prefixCls}-${placement}`, {
[`${prefixCls}-${context.status}`]: context.status,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-loading`]: loading
});
const domProps = pickAttrs(restProps, {
attr: true,
aria: true,
data: true
});
const info = {
key: context?.key,
status: context?.status,
extraInfo: context?.extraInfo
};
// ============================= process content ==============================
const memoedContent = React.useMemo(() => contentRender ? contentRender(content, info) : content, [content, contentRender, info.key, info.status, info.extraInfo]);
const mergeTyping = typeof typing === 'function' ? typing(content, info) : typing;
const usingInnerAnimation = !!mergeTyping && typeof memoedContent === 'string';
/**
* 1、启用内置动画的情况下,由 TypingContent 来负责通知。
* 2、不启用内置动画的情况下,也应当有一个回调来反映 content 的变化。
* 没有动画,则 content 的变化、渲染是全量的,等同于动画是瞬时完成的,合该用 onTypingComplete 来通知变化。
* 3、流式输入 content 的场景下,应当在流式结束时(streaming === false)才执行 onTypingComplete,
* 保证一次流式传输归属于一个动画周期。
**/
React.useEffect(() => {
if (usingInnerAnimation) return;
if (streaming) return;
content && onTypingComplete?.(content);
}, [memoedContent, usingInnerAnimation, streaming]);
// ============================= render ==============================
const _footerPlacement = footerPlacement || (placement === 'start' ? 'outer-start' : 'outer-end');
const isEditing = typeof editable === 'boolean' ? editable : editable.editing;
const renderContent = () => {
if (loading) return loadingRender ? loadingRender() : /*#__PURE__*/React.createElement(Loading, {
prefixCls: prefixCls
});
const _content = /*#__PURE__*/React.createElement(React.Fragment, null, usingInnerAnimation ? /*#__PURE__*/React.createElement(TypingContent, {
prefixCls: prefixCls,
streaming: streaming,
typing: mergeTyping,
content: memoedContent,
onTyping: onTyping,
onTypingComplete: onTypingComplete
}) : memoedContent);
const isFooterIn = _footerPlacement.includes('inner');
return /*#__PURE__*/React.createElement("div", {
className: getSlotClassName('body'),
style: getSlotStyle('body')
}, renderHeader(), /*#__PURE__*/React.createElement("div", {
style: {
...contextConfig.styles.content,
...styles.content
},
className: clsx(`${prefixCls}-content`, `${prefixCls}-content-${variant}`, contextConfig.classNames.content, classNames.content, {
[`${prefixCls}-content-${context?.status}`]: context?.status,
[`${prefixCls}-content-${shape}`]: variant !== 'borderless',
[`${prefixCls}-content-editing`]: isEditing,
[`${prefixCls}-content-string`]: typeof memoedContent === 'string'
})
}, isEditing ? /*#__PURE__*/React.createElement(EditableContent, {
prefixCls: prefixCls,
content: content,
okText: editable?.okText,
cancelText: editable?.cancelText,
onEditConfirm: onEditConfirm,
onEditCancel: onEditCancel
}) : /*#__PURE__*/React.createElement(React.Fragment, null, isFooterIn ? /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-content-with-footer`)
}, _content) : _content, isFooterIn && renderFooter())), !isEditing && !isFooterIn && renderFooter());
};
const getSlotClassName = slotType => clsx(`${prefixCls}-${slotType}`, contextConfig.classNames[slotType], classNames[slotType]);
const getSlotStyle = slotType => ({
...contextConfig.styles[slotType],
...styles[slotType]
});
const renderSlot = slot => typeof slot === 'function' ? slot(content, info) : slot;
const renderAvatar = () => {
if (!avatar) return null;
return /*#__PURE__*/React.createElement("div", {
className: getSlotClassName('avatar'),
style: getSlotStyle('avatar')
}, renderSlot(avatar));
};
const renderExtra = () => {
if (!extra) return null;
return /*#__PURE__*/React.createElement("div", {
className: getSlotClassName('extra'),
style: getSlotStyle('extra')
}, renderSlot(extra));
};
const renderHeader = () => {
if (!header) return null;
return /*#__PURE__*/React.createElement("div", {
className: getSlotClassName('header'),
style: getSlotStyle('header')
}, renderSlot(header));
};
const renderFooter = () => {
if (!footer) return null;
const cls = clsx(getSlotClassName('footer'), {
[`${prefixCls}-footer-start`]: _footerPlacement.includes('start'),
[`${prefixCls}-footer-end`]: _footerPlacement.includes('end')
});
return /*#__PURE__*/React.createElement("div", {
className: cls,
style: getSlotStyle('footer')
}, renderSlot(footer));
};
return /*#__PURE__*/React.createElement("div", _extends({
className: rootMergedCls,
style: rootMergedStyle
}, restProps, domProps, {
ref: rootDiv
}), renderAvatar(), renderContent(), !isEditing && !loading && renderExtra());
};
const ForwardBubble = /*#__PURE__*/React.forwardRef(Bubble);
if (process.env.NODE_ENV !== 'production') {
ForwardBubble.displayName = 'Bubble';
}
export default ForwardBubble;