@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.
432 lines • 19.2 kB
JavaScript
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;
};
import React, { useMemo, useCallback } from 'react';
import cls from 'classnames';
import MarkdownRender from '../../markdownRender';
import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/aiChatDialogue/constants';
import { Image } from '../../index';
import { IconAlertCircle, IconCode, IconExcel, IconFile, IconPdf, IconSendMsgStroked, IconVideo, IconWord, IconWrench } from '@douyinfe/semi-icons';
import { ReasoningWidget } from './contentItem/reasoning';
import { AnnotationWidget } from './contentItem/annotation';
import { ReferenceWidget } from './contentItem/reference';
import Code from './contentItem/code';
import LocaleConsumer from "../../locale/localeConsumer";
import { messageToChatInput } from '@douyinfe/semi-foundation/lib/es/aiChatDialogue/dataAdapter';
import { escapeHtmlInMarkdown } from '@douyinfe/semi-foundation/lib/es/utils/escapeHtml';
const {
PREFIX_CONTENT
} = cssClasses;
const {
STATUS,
MODE,
ROLE,
MESSAGE_ITEM_TYPE,
TEXT_TYPES,
TOOL_CALL_TYPES,
DOCUMENT_TYPES,
IMAGE_TYPES,
PDF_TYPES,
EXCEL_TYPES,
CODE_TYPES,
VIDEO_TYPES
} = strings;
const ImageAttachment = /*#__PURE__*/React.memo(props => {
const {
src,
isList,
msg,
onImageClick,
isLastImage
} = props;
return /*#__PURE__*/React.createElement(Image, {
className: cls(`${PREFIX_CONTENT}-img`, {
[`${PREFIX_CONTENT}-img-list`]: isList,
[`${PREFIX_CONTENT}-img-last`]: isLastImage
}),
src: src,
onClick: () => {
onImageClick && onImageClick(msg);
}
});
});
const FileAttachment = /*#__PURE__*/React.memo(props => {
var _a, _b, _c;
const {
onFileClick,
disabledFileItemClick,
role,
onReferenceClick,
showReference,
isLastFile
} = props,
restProps = __rest(props, ["onFileClick", "disabledFileItemClick", "role", "onReferenceClick", "showReference", "isLastFile"]);
const suffix = (_a = restProps === null || restProps === void 0 ? void 0 : restProps.filename) === null || _a === void 0 ? void 0 : _a.split('.').pop();
const realType = suffix !== null && suffix !== void 0 ? suffix : (_c = (_b = restProps === null || restProps === void 0 ? void 0 : restProps.fileInstance) === null || _b === void 0 ? void 0 : _b.type) === null || _c === void 0 ? void 0 : _c.split('/').pop();
const renderFileIcon = useCallback((type, props) => {
let icon = null;
let typeCls = '';
if (DOCUMENT_TYPES.includes(type)) {
typeCls = 'word';
icon = /*#__PURE__*/React.createElement(IconWord, {
size: "extra-large",
className: `${PREFIX_CONTENT}-file-icon`
});
} else if (IMAGE_TYPES.includes(type)) {
typeCls = 'image';
icon = /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-file-icon`,
style: {
backgroundImage: `url(${props.file_url})`
}
});
} else if (PDF_TYPES.includes(type)) {
typeCls = 'pdf';
icon = /*#__PURE__*/React.createElement(IconPdf, {
size: "extra-large",
className: `${PREFIX_CONTENT}-file-icon`
});
} else if (EXCEL_TYPES.includes(type)) {
typeCls = 'excel';
icon = /*#__PURE__*/React.createElement(IconExcel, {
size: "extra-large",
className: `${PREFIX_CONTENT}-file-icon`
});
} else if (CODE_TYPES.includes(type)) {
typeCls = 'code';
icon = /*#__PURE__*/React.createElement(IconCode, {
size: "extra-large",
className: `${PREFIX_CONTENT}-file-icon`
});
} else if (VIDEO_TYPES.includes(type)) {
typeCls = 'video';
icon = /*#__PURE__*/React.createElement(IconVideo, {
size: "extra-large",
className: `${PREFIX_CONTENT}-file-icon`
});
} else {
typeCls = 'default';
icon = /*#__PURE__*/React.createElement(IconFile, {
size: "extra-large",
className: `${PREFIX_CONTENT}-file-icon`
});
}
return /*#__PURE__*/React.createElement("div", {
className: cls(`${PREFIX_CONTENT}-file-icon-wrapper`, {
[`${PREFIX_CONTENT}-file-icon-${typeCls}`]: typeCls
})
}, icon);
}, []);
const handleFileClick = useCallback(e => {
onFileClick === null || onFileClick === void 0 ? void 0 : onFileClick(restProps);
if (disabledFileItemClick) {
e.preventDefault();
return;
}
}, [onFileClick, disabledFileItemClick, restProps]);
const handleReferenceClick = useCallback(e => {
onReferenceClick === null || onReferenceClick === void 0 ? void 0 : onReferenceClick({
name: restProps === null || restProps === void 0 ? void 0 : restProps.filename,
url: restProps === null || restProps === void 0 ? void 0 : restProps.file_url
});
e.preventDefault();
}, [onReferenceClick, restProps]);
return /*#__PURE__*/React.createElement("a", {
href: restProps === null || restProps === void 0 ? void 0 : restProps.file_url,
target: "_blank",
onClick: handleFileClick,
className: cls(`${PREFIX_CONTENT}-file`, {
[`${PREFIX_CONTENT}-file-last`]: isLastFile
}),
rel: "noreferrer"
}, renderFileIcon(realType, restProps), /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-file-info`
}, /*#__PURE__*/React.createElement("span", {
className: cls(`${PREFIX_CONTENT}-file-title`, {
[`${PREFIX_CONTENT}-file-title-ellipsis`]: role === ROLE.USER && showReference
})
}, restProps === null || restProps === void 0 ? void 0 : restProps.filename), /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-file-metadata`
}, /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-file-type`
}, realType), ' ', restProps === null || restProps === void 0 ? void 0 : restProps.size)), role === ROLE.USER && showReference && (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
React.createElement("div", {
className: `${PREFIX_CONTENT}-icon-reference`,
role: "button",
tabIndex: 0,
onClick: handleReferenceClick
}, /*#__PURE__*/React.createElement(IconSendMsgStroked, null))));
});
const ToolCallWidget = /*#__PURE__*/React.memo(props => {
const {
name
} = props;
return /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-tool-call`
}, /*#__PURE__*/React.createElement(IconWrench, null), name, " ", props.arguments);
});
const DialogueContent = /*#__PURE__*/React.memo(props => {
const {
message,
customRenderFunc,
role: roleInfo,
mode,
markdownRenderProps,
editing,
messageEditRender,
showReference,
onFileClick,
onImageClick,
disabledFileItemClick,
renderDialogueContentItem,
onAnnotationClick,
onReferenceClick,
escapeHtml
} = props;
const {
content,
role,
status,
references
} = message;
const shouldEscapeHtml = escapeHtml && role === ROLE.USER;
const markdownComponents = useMemo(() => Object.assign({
'code': Code
}, markdownRenderProps === null || markdownRenderProps === void 0 ? void 0 : markdownRenderProps.components), [markdownRenderProps]);
const wrapCls = useMemo(() => {
const isUser = role === ROLE.USER;
const bubble = mode === MODE.BUBBLE;
const userBubble = mode === MODE.USER_BUBBLE && isUser;
return cls({
[`${PREFIX_CONTENT}`]: true,
[`${PREFIX_CONTENT}-${mode}`]: bubble || userBubble,
[`${PREFIX_CONTENT}-no-bubble`]: !(bubble || userBubble),
[`${PREFIX_CONTENT}-user`]: isUser,
[`${PREFIX_CONTENT}-error`]: status === STATUS.FAILED && (bubble || userBubble)
});
}, [role, status, mode]);
const customRenderer = useCallback((type, index, item) => {
const customRendererFunc = renderDialogueContentItem === null || renderDialogueContentItem === void 0 ? void 0 : renderDialogueContentItem[type];
if (customRendererFunc) {
let renderer;
// 工具调用类型可从嵌套映射按函数名优先匹配
if (TOOL_CALL_TYPES.includes(type)) {
const toolCallItem = item;
const functionName = toolCallItem === null || toolCallItem === void 0 ? void 0 : toolCallItem.name;
if (typeof customRendererFunc === 'object' && functionName) {
const nestedRenderer = customRendererFunc === null || customRendererFunc === void 0 ? void 0 : customRendererFunc[functionName];
if (nestedRenderer) {
renderer = nestedRenderer;
}
}
}
// 兜底:如果没有匹配到嵌套渲染器且本身是函数,则使用之
if (!renderer && typeof customRendererFunc === 'function') {
renderer = customRendererFunc;
}
if (renderer) {
return /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-custom-renderer`,
key: `index-${index}`
}, renderer(item, message));
}
}
return null;
}, [renderDialogueContentItem, message]);
const renderMarkdown = useCallback((text, key) => {
if (text !== '') {
const rawText = shouldEscapeHtml ? escapeHtmlInMarkdown(text) : text;
return /*#__PURE__*/React.createElement("div", {
className: wrapCls,
key: key
}, /*#__PURE__*/React.createElement(MarkdownRender, Object.assign({
format: 'md',
raw: rawText,
components: markdownComponents
}, markdownRenderProps)), role === ROLE.USER && showReference && (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
React.createElement("div", {
className: `${PREFIX_CONTENT}-icon-reference`,
role: "button",
tabIndex: 0,
onClick: () => onReferenceClick === null || onReferenceClick === void 0 ? void 0 : onReferenceClick({
type: 'text',
content: text
})
}, /*#__PURE__*/React.createElement(IconSendMsgStroked, null))));
}
return null;
}, [wrapCls, markdownComponents, markdownRenderProps, role, onReferenceClick, showReference, shouldEscapeHtml]);
const renderMessage = useCallback((msg, index) => {
var _a;
if (typeof msg.content === 'string') {
return renderMarkdown(msg.content, `msg-${index}`);
}
const inner = (_a = msg.content) !== null && _a !== void 0 ? _a : [];
const isImageList = inner.filter(i => (i === null || i === void 0 ? void 0 : i.type) === MESSAGE_ITEM_TYPE.INPUT_IMAGE).length > 1;
return inner.map((i, innerIdx) => {
var _a, _b;
const customNode = customRenderer(i === null || i === void 0 ? void 0 : i.type, index, i);
if (customNode) return customNode;
if (TEXT_TYPES.includes(i === null || i === void 0 ? void 0 : i.type)) {
const annotation = i.annotations;
// 过滤掉 file_citation 和 container_file_citation 类型的 annotation
const filteredAnnotation = annotation && annotation.length > 0 && annotation.filter(item => item.type !== 'file_citation' && item.type !== 'container_file_citation');
return /*#__PURE__*/React.createElement(React.Fragment, {
key: `msg-${index}-${innerIdx}`
}, filteredAnnotation && filteredAnnotation.length > 0 && /*#__PURE__*/React.createElement(AnnotationWidget, {
annotation: filteredAnnotation,
// todo: 需要支持动态配置
maxCount: 15,
onClick: () => onAnnotationClick(filteredAnnotation)
}), renderMarkdown(i.text || '', `msg-${index}-${innerIdx}`), renderMarkdown(i.refusal || '', `msg-${index}-${innerIdx}-refusal`));
}
if ((i === null || i === void 0 ? void 0 : i.type) === MESSAGE_ITEM_TYPE.INPUT_IMAGE) {
const nextItemType = (_a = inner[innerIdx + 1]) === null || _a === void 0 ? void 0 : _a.type;
const isLastImage = innerIdx === inner.length - 1 || nextItemType === MESSAGE_ITEM_TYPE.INPUT_FILE;
return /*#__PURE__*/React.createElement(React.Fragment, {
key: `msg-${index}-${innerIdx}`
}, /*#__PURE__*/React.createElement(ImageAttachment, {
src: i.image_url,
isList: isImageList,
msg: i,
onImageClick: onImageClick,
isLastImage: isLastImage
}), nextItemType === MESSAGE_ITEM_TYPE.INPUT_FILE && /*#__PURE__*/React.createElement("br", null));
}
if ((i === null || i === void 0 ? void 0 : i.type) === MESSAGE_ITEM_TYPE.INPUT_FILE) {
const nextItemType = (_b = inner[innerIdx + 1]) === null || _b === void 0 ? void 0 : _b.type;
const isLastFile = innerIdx === inner.length - 1 || nextItemType === MESSAGE_ITEM_TYPE.INPUT_IMAGE;
return /*#__PURE__*/React.createElement(React.Fragment, {
key: `msg-${index}-${innerIdx}`
}, /*#__PURE__*/React.createElement(FileAttachment, Object.assign({}, i, {
onFileClick: onFileClick,
disabledFileItemClick: !!disabledFileItemClick,
role: role,
onReferenceClick: onReferenceClick,
showReference: showReference,
isLastFile: isLastFile
})), nextItemType === MESSAGE_ITEM_TYPE.INPUT_IMAGE && /*#__PURE__*/React.createElement("br", null));
}
return null;
});
}, [renderMarkdown, customRenderer, onAnnotationClick, onImageClick, onFileClick, disabledFileItemClick, role, onReferenceClick, showReference]);
const renderToolCall = useCallback((item, index) => (/*#__PURE__*/React.createElement(ToolCallWidget, Object.assign({
key: `tool-${index}`
}, item))), []);
const builtinRenderers = useMemo(() => ({
[MESSAGE_ITEM_TYPE.MESSAGE]: (item, index) => renderMessage(item, index),
[MESSAGE_ITEM_TYPE.REASONING]: (item, index) => (/*#__PURE__*/React.createElement(ReasoningWidget, {
key: `reason-${index}`,
summary: item.summary,
content: item.content,
status: item.status,
markdownRenderProps: markdownRenderProps
})),
[MESSAGE_ITEM_TYPE.FUNCTION_CALL]: renderToolCall,
[MESSAGE_ITEM_TYPE.CUSTOM_TOOL_CALL]: renderToolCall
}), [renderMessage, markdownRenderProps, renderToolCall]);
const loadingNode = useMemo(() => {
const isLoading = [STATUS.QUEUED, STATUS.IN_PROGRESS, STATUS.INCOMPLETE].includes(status);
const isOutputExist = content && (content === null || content === void 0 ? void 0 : content.length) > 0 || message.output_text;
// 如果内容为空,且没有 output_text,则显示 loading
// If the content is empty and there is no output_text, it will display loading
if (isLoading && !isOutputExist) {
return /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-loading`
}, /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-loading-item`
}), /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-loading-item`
}), /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-loading-item`
}), /*#__PURE__*/React.createElement("span", {
className: `${PREFIX_CONTENT}-loading-text`
}, /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "AIChatDialogue"
}, locale => locale['loading'])));
} else {
return null;
}
}, [status, content, message.output_text]);
const node = useMemo(() => {
if (editing) {
return messageEditRender === null || messageEditRender === void 0 ? void 0 : messageEditRender(messageToChatInput(message));
} else {
let realContent;
const textContent = typeof content === 'string' ? content : message.output_text;
if (textContent) {
const defaultRenderer = renderDialogueContentItem === null || renderDialogueContentItem === void 0 ? void 0 : renderDialogueContentItem['default'];
if (typeof defaultRenderer === 'function') {
realContent = /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-custom-renderer`
}, defaultRenderer(textContent, message));
} else {
const rawText = shouldEscapeHtml ? escapeHtmlInMarkdown(textContent) : textContent;
realContent = /*#__PURE__*/React.createElement("div", {
className: wrapCls
}, /*#__PURE__*/React.createElement(MarkdownRender, Object.assign({
format: 'md',
raw: rawText,
components: markdownComponents
}, markdownRenderProps)), role === ROLE.USER && showReference && (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
React.createElement("div", {
className: `${PREFIX_CONTENT}-icon-reference`,
role: "button",
tabIndex: 0,
onClick: () => onReferenceClick === null || onReferenceClick === void 0 ? void 0 : onReferenceClick({
type: 'text',
content: textContent
})
}, /*#__PURE__*/React.createElement(IconSendMsgStroked, null))));
}
} else if (Array.isArray(content)) {
realContent = content.map((item, index) => {
const typeKey = item === null || item === void 0 ? void 0 : item.type;
const effectiveType = typeKey !== null && typeKey !== void 0 ? typeKey : MESSAGE_ITEM_TYPE.MESSAGE;
// User defined rendering first
const customNode = customRenderer(effectiveType, index, item);
if (customNode) return customNode;
// Then builtin rendering
const renderer = builtinRenderers[effectiveType];
if (renderer) return renderer(item, index);
return null;
});
}
return /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-wrapper`
}, (status === STATUS.FAILED || status === STATUS.CANCELLED) && /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-failed`
}, /*#__PURE__*/React.createElement(IconAlertCircle, null)), /*#__PURE__*/React.createElement("div", {
className: `${PREFIX_CONTENT}-inner`
}, realContent));
}
}, [status, content, editing, message, role, messageEditRender, markdownRenderProps, wrapCls, markdownComponents, builtinRenderers, customRenderer, renderDialogueContentItem, showReference, onReferenceClick, shouldEscapeHtml]);
if (customRenderFunc) {
return customRenderFunc({
message,
role: roleInfo,
defaultContent: node,
className: wrapCls
});
} else {
return /*#__PURE__*/React.createElement("div", {
className: cls(`${PREFIX_CONTENT}`, {
[`${PREFIX_CONTENT}-editing`]: editing
})
}, references && references.length > 0 && !editing && /*#__PURE__*/React.createElement(ReferenceWidget, {
references: references
}), node, loadingNode);
}
});
export default DialogueContent;