UNPKG

@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
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;