feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -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 {
/** 是否在浏览器视窗内truefalse不在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>
);
};

View File

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

View File

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