feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FileTypeEnum } from '@coze-common/chat-core/shared/const';
|
||||
|
||||
import EXCELSuccess from '../../../../../assets/file/xlsx-success.svg';
|
||||
import EXCELFail from '../../../../../assets/file/xlsx-fail.svg';
|
||||
import VIDEOSuccess from '../../../../../assets/file/video-success.svg';
|
||||
import VIDEOFail from '../../../../../assets/file/video-fail.svg';
|
||||
import TXTSuccess from '../../../../../assets/file/txt-success.svg';
|
||||
import TXTFail from '../../../../../assets/file/txt-fail.svg';
|
||||
import PPTSuccess from '../../../../../assets/file/ppt-success.svg';
|
||||
import PPTFail from '../../../../../assets/file/ppt-fail.svg';
|
||||
import PDFSuccess from '../../../../../assets/file/pdf-success.svg';
|
||||
import PDFFail from '../../../../../assets/file/pdf-fail.svg';
|
||||
import DOCXSuccess from '../../../../../assets/file/docx-success.svg';
|
||||
import DOCXFail from '../../../../../assets/file/docx-fail.svg';
|
||||
import DefaultUnknownSuccess from '../../../../../assets/file/default-unknown-success.svg';
|
||||
import DefaultUnknownFail from '../../../../../assets/file/default-unknown-fail.svg';
|
||||
import CSVSuccess from '../../../../../assets/file/csv-success.svg';
|
||||
import CSVFail from '../../../../../assets/file/csv-fail.svg';
|
||||
import CODESuccess from '../../../../../assets/file/code-success.svg';
|
||||
import CODEFail from '../../../../../assets/file/code-fail.svg';
|
||||
import AUDIOSuccess from '../../../../../assets/file/audio-success.svg';
|
||||
import AUDIOFail from '../../../../../assets/file/audio-fail.svg';
|
||||
import ARCHIVESuccess from '../../../../../assets/file/archive-success.svg';
|
||||
import ARCHIVEFail from '../../../../../assets/file/archive-fail.svg';
|
||||
|
||||
export const SUCCESS_FILE_ICON_MAP = {
|
||||
[FileTypeEnum.CSV]: CSVSuccess,
|
||||
[FileTypeEnum.DOCX]: DOCXSuccess,
|
||||
[FileTypeEnum.EXCEL]: EXCELSuccess,
|
||||
[FileTypeEnum.PDF]: PDFSuccess,
|
||||
[FileTypeEnum.AUDIO]: AUDIOSuccess,
|
||||
[FileTypeEnum.VIDEO]: VIDEOSuccess,
|
||||
[FileTypeEnum.ARCHIVE]: ARCHIVESuccess,
|
||||
[FileTypeEnum.CODE]: CODESuccess,
|
||||
[FileTypeEnum.TXT]: TXTSuccess,
|
||||
[FileTypeEnum.PPT]: PPTSuccess,
|
||||
[FileTypeEnum.DEFAULT_UNKNOWN]: DefaultUnknownSuccess,
|
||||
};
|
||||
|
||||
export const FAIL_FILE_ICON_MAP = {
|
||||
[FileTypeEnum.CSV]: CSVFail,
|
||||
[FileTypeEnum.DOCX]: DOCXFail,
|
||||
[FileTypeEnum.EXCEL]: EXCELFail,
|
||||
[FileTypeEnum.PDF]: PDFFail,
|
||||
[FileTypeEnum.AUDIO]: AUDIOFail,
|
||||
[FileTypeEnum.VIDEO]: VIDEOFail,
|
||||
[FileTypeEnum.ARCHIVE]: ARCHIVEFail,
|
||||
[FileTypeEnum.CODE]: CODEFail,
|
||||
[FileTypeEnum.TXT]: TXTFail,
|
||||
[FileTypeEnum.PPT]: PPTFail,
|
||||
[FileTypeEnum.DEFAULT_UNKNOWN]: DefaultUnknownFail,
|
||||
};
|
||||
|
||||
export const FILE_CARD_WIDTH = 280;
|
||||
export const PERCENT_DENOMINATOR = 100;
|
||||
@@ -0,0 +1,3 @@
|
||||
.chat-uikit-file-card-progress-animation {
|
||||
transition: width 0.3s linear;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
IconCozCopy,
|
||||
IconCozCross,
|
||||
IconCozRefresh,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Typography } from '@coze-arch/coze-design';
|
||||
import { Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { UIKitTooltip } from '../../../../common/tooltips';
|
||||
import { getFileExtensionAndName } from '../../../../../utils/file-name';
|
||||
import { convertBytes } from '../../../../../utils/convert-bytes';
|
||||
import {
|
||||
typeSafeFileCardNameVariants,
|
||||
typeSafeFileCardVariants,
|
||||
} from './variants';
|
||||
import { type IFileCardProps } from './type';
|
||||
import {
|
||||
FAIL_FILE_ICON_MAP,
|
||||
FILE_CARD_WIDTH,
|
||||
PERCENT_DENOMINATOR,
|
||||
SUCCESS_FILE_ICON_MAP,
|
||||
} from './constants';
|
||||
|
||||
import './file-card.less';
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
const FileCard: FC<IFileCardProps> = props => {
|
||||
const {
|
||||
file,
|
||||
attributeKeys,
|
||||
tooltipsCopywriting,
|
||||
readonly,
|
||||
onCancel,
|
||||
onCopy,
|
||||
onRetry,
|
||||
className,
|
||||
layout,
|
||||
showBackground,
|
||||
} = props;
|
||||
|
||||
const { statusKey, statusEnum, percentKey } = attributeKeys;
|
||||
|
||||
const percent = file[percentKey];
|
||||
|
||||
const fileIconMap = [statusEnum.cancelEnum, statusEnum.failEnum].includes(
|
||||
file[statusKey],
|
||||
)
|
||||
? FAIL_FILE_ICON_MAP
|
||||
: SUCCESS_FILE_ICON_MAP;
|
||||
|
||||
const buttonsVisible = !readonly;
|
||||
const { extension, nameWithoutExtension } = getFileExtensionAndName(
|
||||
file.file_name,
|
||||
);
|
||||
|
||||
const isCanceled = file[statusKey] === statusEnum.cancelEnum;
|
||||
|
||||
return (
|
||||
<div
|
||||
// className={classNames(className, 'chat-uikit-file-card', {
|
||||
// 'chat-uikit-file-card--error': file[statusKey] === statusEnum.failEnum,
|
||||
// 'chat-uikit-file-card-pc': layout === Layout.PC,
|
||||
// 'chat-uikit-file-card-mobile': layout === Layout.MOBILE,
|
||||
// '!coz-bg-image-bots !coz-stroke-image-bots':
|
||||
// showBackground && file[statusKey] !== statusEnum.failEnum,
|
||||
// })}
|
||||
className={classNames(
|
||||
typeSafeFileCardVariants({
|
||||
isError: file[statusKey] === statusEnum.failEnum,
|
||||
layout: layout === Layout.PC ? 'pc' : 'mobile',
|
||||
showBackground,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={fileIconMap[file.file_type]}
|
||||
// chat-uikit-file-card__icon
|
||||
className="h-[32px] w-[32px]"
|
||||
></img>
|
||||
<div
|
||||
// chat-uikit-file-card__info
|
||||
className="flex flex-1 flex-col ml-8px overflow-hidden"
|
||||
>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip:
|
||||
layout === Layout.MOBILE
|
||||
? false
|
||||
: {
|
||||
opts: {
|
||||
content: file.file_name,
|
||||
style: { wordWrap: 'break-word' },
|
||||
},
|
||||
},
|
||||
suffix: extension,
|
||||
}}
|
||||
// chat-uikit-file-card__info__name
|
||||
// chat-uikit-file-card__info__name_pc
|
||||
// chat-uikit-file-card__info__name_mobile
|
||||
className={typeSafeFileCardNameVariants({
|
||||
isCanceled,
|
||||
layout: layout === Layout.PC ? 'pc' : 'mobile',
|
||||
})}
|
||||
>
|
||||
{nameWithoutExtension}
|
||||
</Typography.Text>
|
||||
<span
|
||||
// chat-uikit-file-card__info__size
|
||||
className={classNames(
|
||||
'text-base font-normal leading-[16px]',
|
||||
isCanceled ? 'coz-fg-dim' : 'coz-fg-secondary',
|
||||
)}
|
||||
>
|
||||
{convertBytes(file.file_size)}
|
||||
</span>
|
||||
</div>
|
||||
{buttonsVisible ? (
|
||||
<>
|
||||
<div
|
||||
// chat-uikit-file-card__buttons
|
||||
className="ml-8px"
|
||||
>
|
||||
{file[statusKey] === statusEnum.uploadingEnum && (
|
||||
<UIKitTooltip
|
||||
theme="light"
|
||||
position="top"
|
||||
content={tooltipsCopywriting?.cancel}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
// chat-uikit-file-card__buttons__button
|
||||
icon={
|
||||
<IconCozCross // chat-uikit-file-card__buttons__icon
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
)}
|
||||
{[statusEnum.cancelEnum, statusEnum.failEnum].includes(
|
||||
file[statusKey],
|
||||
) && (
|
||||
<UIKitTooltip
|
||||
theme="light"
|
||||
position="top"
|
||||
content={tooltipsCopywriting?.retry}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
// chat-uikit-file-card__buttons__button
|
||||
|
||||
icon={
|
||||
<IconCozRefresh // chat-uikit-file-card__buttons__icon
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={onRetry}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
)}
|
||||
{file[statusKey] === statusEnum.successEnum && (
|
||||
<UIKitTooltip
|
||||
theme="light"
|
||||
position="top"
|
||||
content={tooltipsCopywriting?.copy}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
// chat-uikit-file-card__buttons__button
|
||||
|
||||
icon={
|
||||
<IconCozCopy // chat-uikit-file-card__buttons__icon
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
)}
|
||||
</div>
|
||||
{file[statusKey] === statusEnum.uploadingEnum && (
|
||||
<div
|
||||
// chat-uikit-file-card__progress-wrap
|
||||
className={classNames(
|
||||
// TODO: ui 补充进度条颜色
|
||||
'coz-fg-hglt-dim absolute top-0 left-0 w-[280px] h-[72px]',
|
||||
'chat-uikit-file-card-progress-animation',
|
||||
)}
|
||||
style={{
|
||||
width: `${FILE_CARD_WIDTH * (percent / PERCENT_DENOMINATOR)}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FileCard.displayName = 'FileCard';
|
||||
|
||||
export default FileCard;
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type IFileAttributeKeys,
|
||||
type IFileCardTooltipsCopyWritingConfig,
|
||||
type IFileInfo,
|
||||
type Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
export interface IFileCardProps {
|
||||
file: IFileInfo;
|
||||
/**
|
||||
* 用于识别成功 / 失败状态的key
|
||||
*/
|
||||
attributeKeys: IFileAttributeKeys;
|
||||
/**
|
||||
* 文案配置
|
||||
*/
|
||||
tooltipsCopywriting?: IFileCardTooltipsCopyWritingConfig;
|
||||
/**
|
||||
* 是否只读
|
||||
*/
|
||||
readonly?: boolean;
|
||||
/**
|
||||
* 取消上传事件回调
|
||||
*/
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* 重试上传事件回调
|
||||
*/
|
||||
onRetry: () => void;
|
||||
/**
|
||||
* 拷贝url事件回调
|
||||
*/
|
||||
onCopy: () => void;
|
||||
className?: string;
|
||||
layout: Layout;
|
||||
showBackground: boolean;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const fileCardVariants = cva(
|
||||
[
|
||||
'select-none',
|
||||
'relative',
|
||||
'overflow-hidden',
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'box-border',
|
||||
'p-12px',
|
||||
'border-[1px]',
|
||||
'border-solid',
|
||||
'rounded-normal',
|
||||
'coz-mg-card',
|
||||
'w-full',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
layout: {
|
||||
pc: ['min-w-[282px]', 'max-w-[320px]'],
|
||||
mobile: ['w-full'],
|
||||
},
|
||||
isError: {
|
||||
true: ['coz-stroke-hglt-red'],
|
||||
false: ['coz-stroke-primary'],
|
||||
},
|
||||
showBackground: {
|
||||
true: ['!coz-bg-image-bots', '!coz-stroke-image-bots'],
|
||||
false: [],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
showBackground: true,
|
||||
isError: false,
|
||||
className: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const fileCardNameVariants = cva(['text-lg', 'font-normal', 'leading-[20px]'], {
|
||||
variants: {
|
||||
layout: {
|
||||
pc: ['w-[180px]'],
|
||||
mobile: ['w-full', 'max-w-[calc(100vw-170px)]'],
|
||||
},
|
||||
isCanceled: {
|
||||
true: ['coz-fg-dim'],
|
||||
false: ['coz-fg-primary'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const typeSafeFileCardVariants: (
|
||||
props: Required<VariantProps<typeof fileCardVariants>>,
|
||||
) => string = fileCardVariants;
|
||||
|
||||
export const typeSafeFileCardNameVariants: (
|
||||
props: Required<VariantProps<typeof fileCardNameVariants>>,
|
||||
) => string = fileCardNameVariants;
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
type IFileAttributeKeys,
|
||||
type IOnRetryUploadParams,
|
||||
type IOnCancelUploadParams,
|
||||
type IOnCopyUploadParams,
|
||||
type IFileCopywritingConfig,
|
||||
type IBaseContentProps,
|
||||
type Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { safeJSONParse } from '../../../utils/safe-json-parse';
|
||||
import { isFile } from '../../../utils/is-file';
|
||||
import FileCard from './components/FileCard';
|
||||
|
||||
export type IProps = IBaseContentProps & {
|
||||
copywriting?: IFileCopywritingConfig;
|
||||
fileAttributeKeys?: IFileAttributeKeys;
|
||||
onCancel?: (params: IOnCancelUploadParams) => void;
|
||||
onRetry?: (params: IOnRetryUploadParams) => void;
|
||||
onCopy?: (params: IOnCopyUploadParams) => void;
|
||||
layout: Layout;
|
||||
showBackground: boolean;
|
||||
};
|
||||
|
||||
export const FileContent: FC<IProps> = props => {
|
||||
const {
|
||||
message,
|
||||
copywriting,
|
||||
fileAttributeKeys,
|
||||
readonly,
|
||||
onCancel,
|
||||
onCopy,
|
||||
onRetry,
|
||||
layout,
|
||||
showBackground,
|
||||
} = props;
|
||||
|
||||
const { content_obj = safeJSONParse(message.content) } = message;
|
||||
|
||||
/**
|
||||
* 判断是否为文件类型的卡片 或者 没有配置file属性config则拒绝使用该卡片
|
||||
*/
|
||||
if (
|
||||
!isFile(content_obj) ||
|
||||
!fileAttributeKeys ||
|
||||
content_obj.file_list.length <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理点击取消上传的事件
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
onCancel?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理重试上传的事件
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
onRetry?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理拷贝文件地址的事件
|
||||
*/
|
||||
const handleCopy = (fileIndex?: number) => {
|
||||
onCopy?.({ message, extra: { fileIndex } });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{content_obj.file_list.map((file, index) => (
|
||||
<FileCard
|
||||
file={file}
|
||||
attributeKeys={fileAttributeKeys}
|
||||
tooltipsCopywriting={copywriting?.tooltips}
|
||||
readonly={readonly}
|
||||
onCancel={handleCancel}
|
||||
onCopy={() => handleCopy(index)}
|
||||
onRetry={handleRetry}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
className={classNames({
|
||||
'mb-[8px]': index < content_obj.file_list.length - 1,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FileContent.displayName = 'FileContent';
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Row, Col } from '@coze-arch/coze-design';
|
||||
import { Image } from '@coze-arch/bot-md-box-adapter/slots';
|
||||
import {
|
||||
type OnImageClickCallback,
|
||||
type OnImageRenderCallback,
|
||||
} from '@coze-arch/bot-md-box-adapter';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export enum CompressAlgorithm {
|
||||
None = 0,
|
||||
Snappy = 1,
|
||||
Zstd = 2,
|
||||
}
|
||||
export interface MsgContentData {
|
||||
card_data?: string;
|
||||
compress?: CompressAlgorithm;
|
||||
}
|
||||
|
||||
export interface ContentBoxEvents {
|
||||
onError?: (err: unknown) => void;
|
||||
onLoadStart?: () => void;
|
||||
onLoadEnd?: () => void;
|
||||
onLoad?: () => Promise<MsgContentData | undefined>;
|
||||
}
|
||||
|
||||
export interface BaseContentBoxProps {
|
||||
/** 是否在浏览器视窗内,true:在,false:不在,undefined:未检测 */
|
||||
inView?: boolean;
|
||||
contentBoxEvents?: ContentBoxEvents;
|
||||
}
|
||||
|
||||
export interface ImageMessageContent {
|
||||
key: string;
|
||||
image_thumb: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
image_ori: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
image_list: ImageMessageContent[];
|
||||
}
|
||||
|
||||
export interface ImageBoxProps extends BaseContentBoxProps {
|
||||
data: ImageContent;
|
||||
eventCallbacks?: {
|
||||
onImageClick?: OnImageClickCallback;
|
||||
onImageRender?: OnImageRenderCallback;
|
||||
};
|
||||
}
|
||||
const getImageBoxGutterAndSpan = (
|
||||
length: number,
|
||||
): {
|
||||
gutter: React.ComponentProps<typeof Row>['gutter'];
|
||||
span: React.ComponentProps<typeof Col>['span'];
|
||||
} => {
|
||||
if (length === 1) {
|
||||
return { gutter: [1, 1], span: 24 };
|
||||
}
|
||||
return { gutter: [2, 2], span: 12 };
|
||||
};
|
||||
|
||||
export const ImageBox: FC<ImageBoxProps> = ({ data, eventCallbacks }) => {
|
||||
const { onImageClick, onImageRender } = eventCallbacks || {};
|
||||
const { image_list = [] } = data || {};
|
||||
|
||||
const layout = getImageBoxGutterAndSpan(image_list?.length);
|
||||
|
||||
return (
|
||||
<div className={classNames('chat-uikit-image-box', 'rounded-normal')}>
|
||||
<Row gutter={layout.gutter}>
|
||||
{image_list.map(({ image_thumb }, index) => (
|
||||
<Col span={layout.span} key={index}>
|
||||
<Image
|
||||
onImageClick={onImageClick}
|
||||
onImageRender={onImageRender}
|
||||
src={image_thumb.url}
|
||||
imageOptions={{
|
||||
squareContainer: true,
|
||||
}}
|
||||
className="object-cover"
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.chat-uikit-image-content {
|
||||
user-select: none;
|
||||
width: 240px;
|
||||
|
||||
img {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-uikit-image-error-boundary {
|
||||
>.semi-image {
|
||||
>img {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-uikit-image-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { Image } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type IOnImageClickParams,
|
||||
type IBaseContentProps,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { safeJSONParse } from '../../../utils/safe-json-parse';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import defaultImage from '../../../assets/image-empty.png';
|
||||
import { ImageBox } from './image-box';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export type IImageMessageContentProps = IBaseContentProps & {
|
||||
onImageClick?: (params: IOnImageClickParams) => void;
|
||||
};
|
||||
|
||||
export const ImageContentImpl: FC<IImageMessageContentProps> = props => {
|
||||
const { message, onImageClick } = props;
|
||||
|
||||
const { content_obj = safeJSONParse(message.content) } = message;
|
||||
|
||||
if (!isImage(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-uikit-image-content">
|
||||
<ImageBox
|
||||
data={{
|
||||
image_list: content_obj?.image_list ?? [],
|
||||
}}
|
||||
eventCallbacks={{
|
||||
onImageClick: (e, eventData) => {
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: { url: eventData.src as string },
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ImageContentImpl.displayName = 'ImageContentImpl';
|
||||
|
||||
export const ImageContent: FC<IImageMessageContentProps> = props => (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<div className="chat-uikit-image-error-boundary">
|
||||
<Image src={defaultImage} preview={false} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ImageContentImpl {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
ImageContent.displayName = 'ImageContent';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './file-content';
|
||||
export * from './image-content';
|
||||
export * from './plain-text-content';
|
||||
export * from './simple-function-content';
|
||||
export * from './suggestion-content';
|
||||
export * from './text-content';
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { type FileMixItem } from '@coze-common/chat-core';
|
||||
import {
|
||||
type IFileAttributeKeys,
|
||||
type IOnCopyUploadParams,
|
||||
type IOnRetryUploadParams,
|
||||
type IOnCancelUploadParams,
|
||||
type IMessage,
|
||||
type IFileCopywritingConfig,
|
||||
type Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import FileCard from '../file-content/components/FileCard';
|
||||
import { isFileMixItem } from '../../../utils/multimodal';
|
||||
|
||||
export interface FileItemListProps {
|
||||
message: IMessage;
|
||||
fileItemList: FileMixItem[];
|
||||
fileAttributeKeys?: IFileAttributeKeys;
|
||||
fileCopywriting?: IFileCopywritingConfig;
|
||||
readonly?: boolean;
|
||||
layout: Layout;
|
||||
showBackground: boolean;
|
||||
onCancel?: (params: IOnCancelUploadParams) => void;
|
||||
onCopy?: (params: IOnCopyUploadParams) => void;
|
||||
onRetry?: (params: IOnRetryUploadParams) => void;
|
||||
}
|
||||
|
||||
export const FileItemList: FC<FileItemListProps> = ({
|
||||
fileItemList,
|
||||
fileAttributeKeys,
|
||||
fileCopywriting,
|
||||
readonly,
|
||||
onRetry,
|
||||
onCancel,
|
||||
onCopy,
|
||||
message,
|
||||
layout,
|
||||
showBackground,
|
||||
}) => {
|
||||
/**
|
||||
* 处理点击取消上传的事件
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
onCancel?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理重试上传的事件
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
onRetry?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理拷贝文件地址的事件
|
||||
*/
|
||||
const handleCopy = () => {
|
||||
onCopy?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileItemList.map(item => {
|
||||
if (isFileMixItem(item) && fileAttributeKeys) {
|
||||
return (
|
||||
<FileCard
|
||||
className="chat-uikit-multi-modal-file-image-content select-none"
|
||||
key={item.file.file_key}
|
||||
file={item.file}
|
||||
attributeKeys={fileAttributeKeys}
|
||||
tooltipsCopywriting={fileCopywriting?.tooltips}
|
||||
readonly={readonly}
|
||||
onCancel={handleCancel}
|
||||
onCopy={handleCopy}
|
||||
onRetry={handleRetry}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { type ImageMixItem } from '@coze-common/chat-core';
|
||||
import {
|
||||
type IOnImageClickParams,
|
||||
type IMessage,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { SingleImageContentWithAutoSize } from '../single-image-content/auto-size';
|
||||
import { ImageBox } from '../image-content/image-box';
|
||||
import { typeSafeMessageBoxInnerVariants } from '../../../variants/message-box-inner-variants';
|
||||
import { makeFakeImageMessage } from '../../../utils/make-fake-image-message';
|
||||
|
||||
interface ImageItemListProps {
|
||||
imageItemList: ImageMixItem[];
|
||||
message: IMessage;
|
||||
onImageClick?: (params: IOnImageClickParams) => void;
|
||||
}
|
||||
|
||||
export const ImageItemList: FC<ImageItemListProps> = ({
|
||||
imageItemList,
|
||||
message,
|
||||
onImageClick,
|
||||
}) => {
|
||||
const handleImageClick = (originUrl: string) => {
|
||||
onImageClick?.({ message, extra: { url: originUrl } });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Boolean(imageItemList.length) &&
|
||||
(imageItemList.length === 1 ? (
|
||||
<SingleImageContentWithAutoSize
|
||||
key={imageItemList[0].image.image_thumb.url}
|
||||
message={makeFakeImageMessage({
|
||||
originMessage: message,
|
||||
url: imageItemList[0].image.image_ori.url,
|
||||
key: imageItemList[0].image.image_ori.url,
|
||||
width: imageItemList[0].image.image_ori.width,
|
||||
height: imageItemList[0].image.image_ori.height,
|
||||
})}
|
||||
onImageClick={onImageClick}
|
||||
className="mb-[16px] rounded-[16px] overflow-hidden"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
// 这里借用了 messageBoxInner 的样式风格
|
||||
className={typeSafeMessageBoxInnerVariants({
|
||||
color: 'whiteness',
|
||||
border: null,
|
||||
tight: true,
|
||||
showBackground: false,
|
||||
})}
|
||||
style={{ width: '240px' }}
|
||||
key={imageItemList[0].image.image_thumb.url}
|
||||
>
|
||||
<ImageBox
|
||||
data={{ image_list: imageItemList.map(item => item.image) }}
|
||||
eventCallbacks={{
|
||||
onImageClick: (_, eventData) =>
|
||||
handleImageClick(eventData.src ?? ''),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
.chat-uikit-multi-modal-file-image-content {
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
type FileMixItem,
|
||||
type TextMixItem,
|
||||
type ImageMixItem,
|
||||
} from '@coze-common/chat-core';
|
||||
import { type GetBotInfo } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { type IImageMessageContentProps } from '../image-content';
|
||||
import { type IProps as IFileContentProps } from '../file-content';
|
||||
import {
|
||||
isFileMixItem,
|
||||
isImageMixItem,
|
||||
isMultimodalContentListLike,
|
||||
isTextMixItem,
|
||||
} from '../../../utils/multimodal';
|
||||
import { TextItemList } from './text-item-list';
|
||||
import { ImageItemList } from './image-item-list';
|
||||
import { FileItemList } from './file-item-list';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export type MultimodalContentProps = IImageMessageContentProps &
|
||||
IFileContentProps & {
|
||||
getBotInfo: GetBotInfo;
|
||||
renderTextContentAddonTop?: ReactNode;
|
||||
isContentLoading: boolean | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 这个组件并不单纯 实际上并不应该叫 Content
|
||||
*/
|
||||
|
||||
// TODO: @liushuoyan 提供开关啊~~
|
||||
export const MultimodalContent: React.FC<MultimodalContentProps> = ({
|
||||
renderTextContentAddonTop,
|
||||
message,
|
||||
getBotInfo,
|
||||
fileAttributeKeys,
|
||||
copywriting: fileCopywriting,
|
||||
onCancel,
|
||||
onCopy,
|
||||
onRetry,
|
||||
readonly,
|
||||
onImageClick,
|
||||
layout,
|
||||
showBackground,
|
||||
isContentLoading,
|
||||
}) => {
|
||||
const { content_obj } = message;
|
||||
if (!isMultimodalContentListLike(content_obj)) {
|
||||
// TODO: broke 的消息应该需要加一个统一的兜底和上报
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileItemList = content_obj.item_list.filter(
|
||||
(item): item is FileMixItem => isFileMixItem(item),
|
||||
);
|
||||
|
||||
const textItemList = content_obj.item_list.filter(
|
||||
(item): item is TextMixItem => isTextMixItem(item),
|
||||
);
|
||||
|
||||
const imageItemList = content_obj.item_list.filter(
|
||||
(item): item is ImageMixItem => isImageMixItem(item),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileItemList
|
||||
fileItemList={fileItemList}
|
||||
fileAttributeKeys={fileAttributeKeys}
|
||||
fileCopywriting={fileCopywriting}
|
||||
readonly={readonly}
|
||||
onRetry={onRetry}
|
||||
onCancel={onCancel}
|
||||
onCopy={onCopy}
|
||||
message={message}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
|
||||
<ImageItemList
|
||||
imageItemList={imageItemList}
|
||||
message={message}
|
||||
onImageClick={onImageClick}
|
||||
/>
|
||||
|
||||
<TextItemList
|
||||
textItemList={textItemList}
|
||||
renderTextContentAddonTop={renderTextContentAddonTop}
|
||||
message={message}
|
||||
showBackground={showBackground}
|
||||
getBotInfo={getBotInfo}
|
||||
isContentLoading={isContentLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MultimodalContent.displayName = 'MultimodalContent';
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type FC } from 'react';
|
||||
|
||||
import { type TextMixItem } from '@coze-common/chat-core';
|
||||
import { type GetBotInfo, type IMessage } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { PlainTextContent } from '../plain-text-content';
|
||||
import { typeSafeMessageBoxInnerVariants } from '../../../variants/message-box-inner-variants';
|
||||
import { isTextMixItem } from '../../../utils/multimodal';
|
||||
|
||||
export interface TextItemListProps {
|
||||
textItemList: TextMixItem[];
|
||||
renderTextContentAddonTop: ReactNode;
|
||||
message: IMessage;
|
||||
showBackground: boolean;
|
||||
getBotInfo: GetBotInfo;
|
||||
isContentLoading: boolean | undefined;
|
||||
}
|
||||
|
||||
export const TextItemList: FC<TextItemListProps> = ({
|
||||
textItemList,
|
||||
renderTextContentAddonTop,
|
||||
message,
|
||||
showBackground,
|
||||
getBotInfo,
|
||||
isContentLoading,
|
||||
}) => (
|
||||
<>
|
||||
{textItemList.map(item => {
|
||||
if (isTextMixItem(item)) {
|
||||
const TextContentAddonTop = renderTextContentAddonTop;
|
||||
const isTextAndMentionedEmpty =
|
||||
!item.text && !message.mention_list.at(0);
|
||||
|
||||
if (isTextAndMentionedEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
/**
|
||||
* TODO: 由于目前设计不支持一条 message 渲染多个 content 这里需要借用一下发送消息的文字气泡背景色
|
||||
* 目前只有用户才能发送 multimodal 消息
|
||||
*/
|
||||
<div
|
||||
className={typeSafeMessageBoxInnerVariants({
|
||||
color: 'primary',
|
||||
border: null,
|
||||
tight: false,
|
||||
showBackground,
|
||||
})}
|
||||
style={{ width: 'fit-content' }}
|
||||
key={item.text}
|
||||
>
|
||||
{TextContentAddonTop}
|
||||
<PlainTextContent
|
||||
isContentLoading={isContentLoading}
|
||||
content={item.text}
|
||||
mentioned={message.mention_list.at(0)}
|
||||
getBotInfo={getBotInfo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
# 介绍
|
||||
该user-text-content比较特殊,目前用户输入的文本应当不支持以markdown语法的富文本,因此需要特殊使用user-text-content这种content类型
|
||||
@@ -0,0 +1,4 @@
|
||||
.chat-uikit-plain-text-content {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { type MessageMentionListFields } from '@coze-common/chat-core/src/message/types';
|
||||
import {
|
||||
type IBaseContentProps,
|
||||
type GetBotInfo,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { ThinkingPlaceholder } from '../../chat';
|
||||
import { isText } from '../../../utils/is-text';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export type IPlainTextMessageContentProps = Omit<
|
||||
IBaseContentProps,
|
||||
'message'
|
||||
> & {
|
||||
getBotInfo: GetBotInfo;
|
||||
content: string;
|
||||
mentioned: MessageMentionListFields['mention_list'][0] | undefined;
|
||||
isContentLoading: boolean | undefined;
|
||||
};
|
||||
|
||||
export const PlainTextContent: FC<IPlainTextMessageContentProps> = props => {
|
||||
const { content, isContentLoading } = props;
|
||||
|
||||
if (!isText(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-uikit-plain-text-content">
|
||||
{isContentLoading ? (
|
||||
<ThinkingPlaceholder className="!p-0 !h-20px" />
|
||||
) : (
|
||||
<span>{`${getMentionBotContent(props)}${content}`}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PlainTextContent.displayName = 'PlainTextContent';
|
||||
|
||||
const getMentionBotContent = ({
|
||||
mentioned,
|
||||
getBotInfo,
|
||||
}: IPlainTextMessageContentProps) => {
|
||||
// 接口真不一定返回了 mention_list
|
||||
if (!mentioned) {
|
||||
return '';
|
||||
}
|
||||
const name = getBotInfo(mentioned.id)?.nickname;
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
return `@${name} `;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { typeSafeJsonParse } from '@coze-common/chat-area-utils';
|
||||
import { IconCozLoading } from '@coze-arch/coze-design/icons';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { isFunctionCall } from '../../../utils/is-function-call';
|
||||
import { type ISimpleFunctionMessageContentProps } from './type';
|
||||
|
||||
export const SimpleFunctionContent: FC<
|
||||
ISimpleFunctionMessageContentProps
|
||||
> = props => {
|
||||
const { message, copywriting } = props;
|
||||
|
||||
const { content } = message;
|
||||
|
||||
const contentObj = typeSafeJsonParse(content, () => undefined);
|
||||
|
||||
if (!isFunctionCall(contentObj, message)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
// chat-uikit-simple-function-content
|
||||
className="coz-fg-hglt select-none flex items-center max-w-[230px] text-xxl leading-[26px]"
|
||||
>
|
||||
<IconCozLoading
|
||||
// chat-uikit-simple-function-content__prefix-icon
|
||||
className="animate-spin"
|
||||
/>
|
||||
<div
|
||||
// chat-uikit-simple-function-content__prefix-text
|
||||
className="mr-[4px] ml-[8px]"
|
||||
>
|
||||
{copywriting?.using ?? 'using'}
|
||||
</div>
|
||||
<Typography.Text
|
||||
// chat-uikit-simple-function-content__plugin-name
|
||||
className="coz-fg-hglt flex-1 text-xxl font-bold leading-[26px]"
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: contentObj.name,
|
||||
style: { wordWrap: 'inherit' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contentObj.name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SimpleFunctionContent.displayName = 'SimpleFunctionContent';
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ISimpleFunctionContentCopywriting,
|
||||
type IBaseContentProps,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
export type ISimpleFunctionMessageContentProps = IBaseContentProps & {
|
||||
copywriting?: ISimpleFunctionContentCopywriting;
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { type ContentType, type Message } from '@coze-common/chat-core';
|
||||
import {
|
||||
safeAsyncThrow,
|
||||
typeSafeJsonParseEnhanced,
|
||||
} from '@coze-common/chat-area-utils';
|
||||
import { Skeleton } from '@coze-arch/coze-design';
|
||||
import { type IImageContent } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { type IImageMessageContentProps } from '../image-content';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import { getImageDisplayAttribute } from '../../../utils/image/get-image-display-attribute';
|
||||
import { useUiKitMessageBoxContext } from '../../../context/message-box';
|
||||
import DefaultImage from '../../../assets/image-default.png';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface ImageInfo {
|
||||
url: string;
|
||||
displayWidth: number;
|
||||
displayHeight: number;
|
||||
}
|
||||
type IBlobImageMap = Record<string, ImageInfo>;
|
||||
|
||||
interface SingleImageContentWithAutoSizeProps
|
||||
extends IImageMessageContentProps {
|
||||
content_obj: IImageContent;
|
||||
}
|
||||
|
||||
export const SingleImageContentWithAutoSize: FC<
|
||||
IImageMessageContentProps
|
||||
> = props => {
|
||||
const { message } = props;
|
||||
|
||||
const {
|
||||
content_obj = typeSafeJsonParseEnhanced<Message<ContentType.Image>>({
|
||||
str: message.content,
|
||||
onParseError: e => {
|
||||
safeAsyncThrow(e.message);
|
||||
},
|
||||
onVerifyError: e => {
|
||||
safeAsyncThrow(e.message);
|
||||
},
|
||||
verifyStruct: (sth: unknown): sth is Message<ContentType.Image> =>
|
||||
Boolean(sth && 'image_list' in { ...sth }),
|
||||
}),
|
||||
} = message;
|
||||
// 类型守卫,一般情况也不影响hooks的顺序问题
|
||||
if (!isImage(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SingleImageContentWithAutoSizeImpl content_obj={content_obj} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 这里这么做是有原因的
|
||||
* 前端计算groupId是通过replyId分组(服务端未ack前是localMessageId)
|
||||
* 因此服务端ack后会导致循环的key发生变化,导致组件unmount -> mount(销毁重建)
|
||||
* 因此需要用比较trick的方式来实现图片展示优化的问题
|
||||
*/
|
||||
const blobImageMap: IBlobImageMap = {};
|
||||
const isBlob = (url: string) => url?.startsWith('blob:');
|
||||
|
||||
const SingleImageContentWithAutoSizeImpl: FC<
|
||||
SingleImageContentWithAutoSizeProps
|
||||
> = props => {
|
||||
const { message, onImageClick, className, content_obj } = props;
|
||||
const { imageAutoSizeContainerWidth = 0 } = useUiKitMessageBoxContext();
|
||||
const localMessageId = message.extra_info.local_message_id;
|
||||
|
||||
// 目前服务端下发的图片 ori = thumb 因此目前用一个就行
|
||||
const currentImageUrl = content_obj?.image_list?.at(0)?.image_ori?.url ?? '';
|
||||
|
||||
const { displayHeight, displayWidth, isCover } = getImageDisplayAttribute(
|
||||
content_obj.image_list.at(0)?.image_ori.width ?? 0,
|
||||
content_obj.image_list.at(0)?.image_ori.height ?? 0,
|
||||
imageAutoSizeContainerWidth,
|
||||
);
|
||||
|
||||
if (isBlob(currentImageUrl) && imageAutoSizeContainerWidth > 0) {
|
||||
blobImageMap[localMessageId] = {
|
||||
url: currentImageUrl,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
};
|
||||
}
|
||||
|
||||
const [imageInfo, setImageInfo] = useState<ImageInfo>(
|
||||
blobImageMap[localMessageId] ?? {
|
||||
url: currentImageUrl,
|
||||
displayWidth,
|
||||
displayHeight,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const preloadImage = new Image();
|
||||
|
||||
if (currentImageUrl.startsWith('http')) {
|
||||
preloadImage.src = currentImageUrl;
|
||||
preloadImage.onload = () => {
|
||||
setImageInfo({
|
||||
url: currentImageUrl,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
preloadImage.onload = null;
|
||||
};
|
||||
}, [currentImageUrl, imageAutoSizeContainerWidth]);
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
loading={isEmpty(imageInfo?.url)}
|
||||
style={{
|
||||
width: imageInfo?.displayWidth,
|
||||
height: imageInfo?.displayHeight,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageInfo?.url ?? DefaultImage}
|
||||
style={{
|
||||
width: imageInfo?.displayWidth,
|
||||
height: imageInfo?.displayHeight,
|
||||
maxWidth: '100%',
|
||||
objectFit: isCover ? 'cover' : undefined,
|
||||
objectPosition: 'left top',
|
||||
}}
|
||||
onClick={e =>
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: {
|
||||
url: imageInfo?.url,
|
||||
},
|
||||
})
|
||||
}
|
||||
className={classNames('block', className, {
|
||||
'cursor-zoom-in': Boolean(onImageClick),
|
||||
})}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
SingleImageContentWithAutoSize.displayName = 'SingleImageContentWithAutoSize';
|
||||
@@ -0,0 +1,19 @@
|
||||
.chat-uikit-single-image-content {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__image {
|
||||
>img {
|
||||
cursor: zoom-in;
|
||||
|
||||
min-width: 48px;
|
||||
max-width: 240px;
|
||||
min-height: 48px;
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, type FC, useEffect } from 'react';
|
||||
|
||||
import { type IImageMessageContentProps } from '../image-content';
|
||||
import { safeJSONParse } from '../../../utils/safe-json-parse';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import { SingleImageContentUI } from './single-image-content-ui';
|
||||
|
||||
import './index.less';
|
||||
|
||||
type IBlobImageMap = Record<string, string>;
|
||||
|
||||
/**
|
||||
* 这里这么做是有原因的
|
||||
* 前端计算groupId是通过replyId分组(服务端未ack前是localMessageId)
|
||||
* 因此服务端ack后会导致循环的key发生变化,导致组件unmount -> mount(销毁重建)
|
||||
* 因此需要用比较trick的方式来实现图片展示优化的问题
|
||||
*/
|
||||
const blobImageMap: IBlobImageMap = {};
|
||||
const isBlob = (url: string) => url?.startsWith('blob:');
|
||||
|
||||
/**
|
||||
* @deprecated 废弃不再维护,请尽快迁移至 SingleImageContentWithAutoSize 组件
|
||||
*/
|
||||
export const SingleImageContent: FC<IImageMessageContentProps> = props => {
|
||||
const { message, onImageClick } = props;
|
||||
|
||||
// @liushuoyan 这里类型大溃败,引入了 any
|
||||
const { content_obj = safeJSONParse(message.content) } = message;
|
||||
|
||||
const localMessageId = message.extra_info.local_message_id;
|
||||
|
||||
// 目前服务端下发的图片 ori = thumb 因此目前用一个就行
|
||||
const currentImageUrl = content_obj?.image_list?.at(0)?.image_ori?.url ?? '';
|
||||
|
||||
if (isBlob(currentImageUrl)) {
|
||||
blobImageMap[localMessageId] = currentImageUrl;
|
||||
}
|
||||
|
||||
const [imageUrl, setImageUrl] = useState<string>(
|
||||
isBlob(currentImageUrl) ? currentImageUrl : blobImageMap[localMessageId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const preloadImage = new Image();
|
||||
if (currentImageUrl.startsWith('http')) {
|
||||
preloadImage.src = currentImageUrl;
|
||||
preloadImage.onload = () => {
|
||||
setImageUrl(currentImageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
preloadImage.onload = null;
|
||||
};
|
||||
}, [currentImageUrl]);
|
||||
|
||||
if (!isImage(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleImageContentUI
|
||||
onClick={originUrl => {
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: { url: originUrl },
|
||||
});
|
||||
}}
|
||||
thumbUrl={imageUrl}
|
||||
originalUrl={imageUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SingleImageContent.displayName = 'SingleImageContent';
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Image } from '@coze-arch/coze-design';
|
||||
|
||||
import EmptyImage from '../../../assets/image-empty.png';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export interface SingleImageContentUIProps {
|
||||
thumbUrl: string;
|
||||
originalUrl: string;
|
||||
onClick?: (originUrl: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SingleImageContentUI: React.FC<SingleImageContentUIProps> = ({
|
||||
thumbUrl,
|
||||
originalUrl,
|
||||
onClick,
|
||||
className,
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(className, 'chat-uikit-single-image-content')}
|
||||
onClick={() => onClick?.(originalUrl)}
|
||||
>
|
||||
<Image
|
||||
src={thumbUrl || EmptyImage}
|
||||
className="chat-uikit-single-image-content__image"
|
||||
/**
|
||||
* 这里不采用 semi Image 组件自带的 preview 功能。传入的 onImageClick 回调中有副作用会拉起 preview 组件
|
||||
*/
|
||||
preview={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
SingleImageContentUI.displayName = 'SingleImageContentUI';
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
// 特别处理 有背景图的情况下 不区分主题深浅色
|
||||
.chat-uikit-suggestion-item-background-mg {
|
||||
&:hover {
|
||||
background-color: rgba(235, 235, 235, 75%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(235, 235, 235, 70%);;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { type IMessage } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { isText } from '../../../../../utils/is-text';
|
||||
import { typeSafeSuggestionItemVariants } from './variants';
|
||||
import './index.less';
|
||||
|
||||
interface ISuggestionItemProps {
|
||||
message?: Pick<IMessage, 'content_obj' | 'sender_id'>;
|
||||
content?: string;
|
||||
readonly?: boolean;
|
||||
showBackground?: boolean;
|
||||
className?: string;
|
||||
color?: 'white' | 'grey';
|
||||
onSuggestionClick?: (sugParam: {
|
||||
text: string;
|
||||
mentionList: { id: string }[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const SuggestionItem: FC<ISuggestionItemProps> = props => {
|
||||
const {
|
||||
content,
|
||||
message,
|
||||
readonly,
|
||||
onSuggestionClick,
|
||||
showBackground,
|
||||
className,
|
||||
color,
|
||||
} = props;
|
||||
const { content_obj = content } = message ?? {};
|
||||
|
||||
if (!isText(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'!bg-[235, 235, 235, 0.75]',
|
||||
typeSafeSuggestionItemVariants({
|
||||
showBackground: Boolean(showBackground),
|
||||
readonly: Boolean(readonly),
|
||||
color: color ?? 'white',
|
||||
}),
|
||||
)}
|
||||
onClick={() => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const senderId = message?.sender_id;
|
||||
onSuggestionClick?.({
|
||||
text: content_obj,
|
||||
mentionList: senderId ? [{ id: senderId }] : [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="w-full">{content_obj}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SuggestionItem.displayName = 'SuggestionItem';
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
const suggestionItemVariants = cva(
|
||||
[
|
||||
'w-fit',
|
||||
'border-[1px]',
|
||||
'border-solid',
|
||||
'rounded-normal',
|
||||
'coz-fg-primary',
|
||||
'py-6px',
|
||||
'px-16px',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'mb-8px',
|
||||
'max-w-full',
|
||||
'text-[14px]',
|
||||
'font-normal',
|
||||
'leading-[20px]',
|
||||
'break-words',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
showBackground: {
|
||||
true: ['coz-bg-image-question', 'coz-stroke-image-bots'],
|
||||
false: ['coz-stroke-plus'],
|
||||
},
|
||||
color: {
|
||||
white: [],
|
||||
grey: [],
|
||||
},
|
||||
readonly: {
|
||||
true: ['cursor-default'],
|
||||
false: ['cursor-pointer'],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
showBackground: false,
|
||||
color: 'white',
|
||||
className: [],
|
||||
},
|
||||
{
|
||||
showBackground: false,
|
||||
color: 'grey',
|
||||
className: ['bg-[var(--coz-mg-secondary)]'],
|
||||
},
|
||||
{
|
||||
readonly: false,
|
||||
showBackground: false,
|
||||
className: [
|
||||
'hover:bg-[var(--coz-mg-secondary-hovered)]',
|
||||
'active:bg-[var(--coz-mg-secondary-pressed)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
readonly: false,
|
||||
showBackground: true,
|
||||
className: ['chat-uikit-suggestion-item-background-mg'],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
type SuggestionItemVariantsProps = VariantProps<typeof suggestionItemVariants>;
|
||||
|
||||
export const typeSafeSuggestionItemVariants: (
|
||||
props: Required<SuggestionItemVariantsProps>,
|
||||
) => string = suggestionItemVariants;
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SuggestionItem } from './components/suggestion-item';
|
||||
|
||||
export { SuggestionItem };
|
||||
@@ -0,0 +1,3 @@
|
||||
.chat-uikit-text-content {
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MouseEvent, type FC, useRef } from 'react';
|
||||
|
||||
import {
|
||||
type IOnImageClickParams,
|
||||
type IOnLinkClickParams,
|
||||
type IBaseContentProps,
|
||||
type MdBoxProps,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
import { Image } from '@coze-arch/bot-md-box-adapter/slots';
|
||||
import { type ImageOptions } from '@coze-arch/bot-md-box-adapter';
|
||||
|
||||
import { CozeLink } from '../../md-box-slots/link';
|
||||
import { CozeImage } from '../../md-box-slots/coze-image';
|
||||
import { LazyCozeMdBox } from '../../common/coze-md-box/lazy';
|
||||
import { isText } from '../../../utils/is-text';
|
||||
import './index.less';
|
||||
|
||||
export type IMessageContentProps = IBaseContentProps & {
|
||||
onImageClick?: (params: IOnImageClickParams) => void;
|
||||
mdBoxProps?: MdBoxProps;
|
||||
enableAutoSizeImage?: boolean;
|
||||
imageOptions?: ImageOptions;
|
||||
onLinkClick?: (
|
||||
params: IOnLinkClickParams,
|
||||
event: MouseEvent<Element, globalThis.MouseEvent>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const TextContent: FC<IMessageContentProps> = props => {
|
||||
const {
|
||||
message,
|
||||
readonly,
|
||||
onImageClick,
|
||||
onLinkClick,
|
||||
mdBoxProps,
|
||||
enableAutoSizeImage,
|
||||
imageOptions,
|
||||
} = props;
|
||||
const MdBoxLazy = LazyCozeMdBox;
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { content } = message;
|
||||
|
||||
if (!isText(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isStreaming = !message.is_finish;
|
||||
const text = content.slice(0, message.broken_pos ?? Infinity);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat-uikit-text-content"
|
||||
data-testid="bot.ide.chat_area.message.text-answer-message-content"
|
||||
ref={contentRef}
|
||||
data-grab-mark={message.message_id}
|
||||
data-grab-source={message.source}
|
||||
>
|
||||
<MdBoxLazy
|
||||
markDown={text}
|
||||
autoFixSyntax={{ autoFixEnding: isStreaming }}
|
||||
showIndicator={isStreaming}
|
||||
smooth={isStreaming}
|
||||
imageOptions={{ forceHttps: !IS_OPEN_SOURCE, ...imageOptions }}
|
||||
eventCallbacks={{
|
||||
onImageClick: (e, eventData) => {
|
||||
eventData.src &&
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: { url: eventData.src },
|
||||
});
|
||||
},
|
||||
onLinkClick: (e, eventData) => {
|
||||
onLinkClick?.(
|
||||
{
|
||||
message,
|
||||
extra: { ...eventData },
|
||||
},
|
||||
e,
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
}}
|
||||
{...mdBoxProps}
|
||||
slots={{
|
||||
Image: enableAutoSizeImage ? CozeImage : Image,
|
||||
Link: CozeLink,
|
||||
...mdBoxProps?.slots,
|
||||
}}
|
||||
></MdBoxLazy>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TextContent.displayName = 'TextContent';
|
||||
Reference in New Issue
Block a user