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,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 React from 'react';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
interface CropperFooterProps {
loading: boolean;
disabledConfig: Record<'upload' | 'submit', boolean>;
handleOpenFileDialog: () => void;
handleCancel: () => void;
handleSubmit: () => void;
}
export const CropperFooter: React.FC<CropperFooterProps> = ({
handleOpenFileDialog,
handleCancel,
handleSubmit,
loading,
disabledConfig,
}) => (
<div className="flex justify-between p-6 coz-bg-max">
<div>
<Button
onClick={handleOpenFileDialog}
color="primary"
disabled={disabledConfig.upload}
>
{I18n.t('bgi_reupload')}
</Button>
{!disabledConfig.submit ? (
<span className="coz-fg-dim ml-3 text-xs">
{I18n.t('bgi_adjust_tooltip_content')}
</span>
) : null}
</div>
<div className="flex gap-2">
<Button
onClick={handleCancel}
color="highlight"
className="!coz-mg-hglt !coz-fg-hglt"
>
{I18n.t('Cancel')}
</Button>
<Button
data-testid="agent-ide.chat_background_img_submit"
onClick={handleSubmit}
loading={loading}
disabled={disabledConfig.submit}
>
{I18n.t('Confirm')}
</Button>
</div>
</div>
);

View File

@@ -0,0 +1,8 @@
.text {
font-size: 8px;
line-height: 18px;
}
.shadow {
text-shadow: 0 0.5px 1px rgba(0, 0, 0, 20%);
}

View File

@@ -0,0 +1,173 @@
/*
* 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 { useMemo } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Tag } from '@coze-arch/coze-design';
import { IconSpin } from '@douyinfe/semi-icons';
import s from './index.module.less';
interface CropperCoverProps {
loading: boolean;
mode: 'pc' | 'mobile';
hasUrl: boolean;
}
const Config = {
pc: [
{
width: 300,
height: 45,
},
{
width: 300,
height: 34,
},
{
width: 73,
height: 24,
},
],
mobile: [
{
width: 186,
height: 63,
},
{
width: 185,
height: 28,
},
{
width: 60,
height: 20,
},
],
};
const BubbleContent = ({
bg,
width,
height,
rounded,
className,
}: {
bg: string;
width: number;
height: number;
rounded: string;
className: string;
}) => (
<div
className={`rounded-${rounded} ${className}`}
style={{
background: bg,
width,
height,
backdropFilter: 'blur(6px)',
}}
></div>
);
const AvaterContent = ({ bg, size }: { bg: string; size: number }) => (
<div
className={`bg-[${bg}] rounded-full`}
style={{
background: bg,
width: size,
height: size,
}}
></div>
);
export const CropperCover: React.FC<CropperCoverProps> = ({
mode,
loading = false,
hasUrl = false,
}) =>
useMemo(
() => (
<div className="w-full h-full p-2 absolute z-[200] pointer-events-none ">
<div className="flex gap-2 mb-6">
<Tag
color="primary"
prefixIcon={null}
// 这里适配背景图色 颜色固定不改变
className="!text-white !bg-[rgba(0,0,0,0.28)]"
>
{mode === 'pc'
? I18n.t('display_on_widescreen')
: I18n.t('display_on_vertical_screen')}
</Tag>
{loading ? (
<Tag
color="primary"
prefixIcon={<IconSpin spin />}
className="!text-white !bg-[rgba(0,0,0,0.28)]"
>
{I18n.t('knowledge_insert_img_009')}
</Tag>
) : null}
</div>
<div className="flex justify-center">
<div className={mode === 'pc' ? 'w-[326px]' : 'w-[205px]'}>
{Config[mode].map((item, index) => {
const notHasBackground = loading || !hasUrl;
const background = notHasBackground
? index === 1
? '#4E40E5'
: '#F4F4F6'
: index === 1
? 'rgba(6, 7, 9, 0.24)'
: 'rgba(255, 255, 255, 0.60)';
return (
<div className="flex gap-1 mb-2" key={index}>
<AvaterContent
size={mode === 'pc' ? 18 : 16}
bg={background}
/>
<div>
<div
className={classNames(s.text, {
[s.shadow]: !notHasBackground,
'coz-fg-dim': notHasBackground,
'text-white': !notHasBackground,
})}
>
{index === 1 ? 'Glory' : 'Coze'}
</div>
<BubbleContent
width={item.width}
height={item.height}
bg={background}
rounded="lg"
className={classNames({
[s['background-border']]: !notHasBackground,
[s.default]: notHasBackground,
})}
/>
</div>
</div>
);
})}
</div>
</div>
</div>
),
[mode, loading, hasUrl],
);

View File

@@ -0,0 +1,99 @@
/*
* 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 } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Popover } from '@coze-arch/coze-design';
import { FIRST_GUIDE_KEY_PREFIX } from '@coze-agent-ide/chat-background-shared';
interface CropperGuideProps {
userId: string;
}
export const CropperGuide: React.FC<CropperGuideProps> = ({ userId }) => {
const showGuide = !window.localStorage.getItem(
`${FIRST_GUIDE_KEY_PREFIX}[${userId}]`,
);
const [position, setPosition] = useState({
x: 0,
y: 0,
});
const [mouseIn, setMouseIn] = useState(false);
const [draggable, setDraggable] = useState(false);
const handleMouseMove = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
setPosition({
x: event.nativeEvent.offsetX,
y: event.nativeEvent.offsetY,
});
};
return (
<>
{mouseIn && showGuide ? (
<Popover
content={
<>
<div className="coz-fg-plus font-semi">
{I18n.t('bgi_adjust_tooltip_title')}
</div>
<div className="coz-fg-dim text-xs">
{I18n.t('bgi_adjust_tooltip_content')}
</div>
</>
}
visible
rePosKey={position.x + position.y}
showArrow
position="top"
>
<div
className="absolute w-4 h-4 z-[300]"
style={{
top: position.y,
left: position.x,
}}
/>
</Popover>
) : null}
{showGuide ? (
<div
className={'absolute w-full h-full z-[300] pointer-events-none'}
onMouseEnter={() => {
setMouseIn(true);
}}
onMouseLeave={() => {
setMouseIn(false);
}}
onClick={() => {
setDraggable(true);
window.localStorage.setItem(
`${FIRST_GUIDE_KEY_PREFIX}[${userId}]`,
'true',
);
}}
onMouseMove={handleMouseMove}
style={{
pointerEvents: draggable ? 'none' : 'auto',
}}
></div>
) : null}
</>
);
};

View File

@@ -0,0 +1,25 @@
.cropper-container {
position: relative;
width: 100%;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
:global {
.cropper-wrap-box {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.cropper-view-box {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
outline: none;
}
.cropper-container {
transition: opacity 300ms cubic-bezier(0.34, 0.69, 0.1, 1);
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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 Cropper, { type ReactCropperElement } from 'react-cropper';
import React, { type RefObject } from 'react';
import { debounce } from 'lodash-es';
import classNames from 'classnames';
import { WithRuleImgBackground } from '@coze-common/chat-uikit';
import { type BackgroundImageDetail } from '@coze-arch/bot-api/developer_api';
import { useCropperImg } from '@coze-agent-ide/chat-background-shared';
import { CropperGuide } from '../cropper-guide';
import { CropperCover } from '../cropper-cover';
import s from './index.module.less';
import 'cropperjs/dist/cropper.css';
interface CropperProps {
url?: string;
cropperRef: RefObject<ReactCropperElement>;
accept?: string;
mode?: 'pc' | 'mobile';
loading: boolean;
setLoading: (loading: boolean) => void;
getUserId: () => string;
backgroundInfo?: BackgroundImageDetail;
}
const CropperImg: React.FC<CropperProps> = ({
url = '',
mode = 'pc',
cropperRef,
loading,
setLoading,
getUserId,
backgroundInfo,
}) => {
const {
handleCrop,
handleReady,
themeColor,
gradientPosition,
size,
onZoom,
cropEnd,
} = useCropperImg({
url,
mode,
cropperRef,
setLoading,
backgroundInfo,
});
const currentBackgroundInfo: BackgroundImageDetail = {
image_url: url,
gradient_position: gradientPosition,
theme_color: themeColor,
};
const userId = getUserId();
const debouncedZoom = debounce(onZoom, 100);
return (
<div
className={`${classNames(
s['cropper-container'],
)} outline outline-1 outline-[#0607091A] hover:outline-[#4E40E5]`}
style={{ background: themeColor, height: size.height, width: size.width }}
>
<CropperGuide userId={userId} />
<CropperCover mode={mode} loading={loading} hasUrl={!!url} />
<WithRuleImgBackground
key={mode}
backgroundInfo={{
web_background_image: mode === 'pc' ? currentBackgroundInfo : {},
mobile_background_image:
mode === 'mobile' ? currentBackgroundInfo : {},
}}
preview
/>
{url ? (
<Cropper
ready={handleReady}
initialAspectRatio={size.width / size.height}
src={url}
style={{ height: size.height, width: size.width }}
background={false} // 是否在容器内显示网格背景
guides={false}
zoom={debouncedZoom}
ref={cropperRef}
dragMode="move" // 图片容器可移动
viewMode={0} // 定义cropper的视图模式0允许裁剪框可以延伸到图片容器之外
modal={false} // 是否在图片和裁剪框之间显示黑色蒙版
center={false}
cropBoxMovable={false} // 是否可以拖拽裁剪框 默认true
cropBoxResizable={false} // 默认true ,是否允许拖动 改变裁剪框大小
highlight={false}
autoCropArea={1}
minCanvasHeight={size.height}
minCropBoxHeight={size.height}
minCropBoxWidth={size.width}
minContainerWidth={size.width}
crop={handleCrop}
cropend={cropEnd}
checkCrossOrigin={false}
checkOrientation={false}
crossOrigin={'anonymous'}
/>
) : null}
</div>
);
};
export default CropperImg;

View File

@@ -0,0 +1,8 @@
.icon {
svg {
width: 24px;
height: 24px;
// todo token颜色
color: #4E40E5;
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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 ReactNode } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Popover } from '@coze-arch/bot-semi';
import { IconUpload, IconInfo } from '@coze-arch/bot-icons';
import { Button } from '@coze-arch/coze-design';
import IMG_SIZE_TIP from '../../assets/image.png';
import s from './index.module.less';
interface DragContentProps {
icon: ReactNode;
title: string;
tip?: ReactNode;
desc: string;
btnText: string;
btnOnClick?: () => void;
}
export const DragContent: React.FC<DragContentProps> = ({
icon,
title,
desc,
btnOnClick,
btnText,
tip,
}) => (
<div className="flex items-center flex-col m-auto">
<div className="mb-4 w-6 h-6">{icon}</div>
<div className="text-xxl text-[#1C1F23] font-medium flex items-center gap-1/2">
{title}
{tip}
</div>
<div className="text-xs text-[#888D92] mb-4 mt-1">{desc}</div>
{btnOnClick ? (
<Button
onClick={btnOnClick}
color="primary"
className="!coz-fg-primary !coz-mg-primary"
>
{btnText}
</Button>
) : null}
</div>
);
interface DragUploadContentProps {
manualUpload?: () => void;
mode?: 'init' | 'fill';
renderEnhancedUpload?: () => ReactNode;
}
export const DragUploadContent: React.FC<DragUploadContentProps> = ({
manualUpload,
renderEnhancedUpload,
mode = 'init',
}) => {
const [dragIn, setDragIn] = useState(false);
const customUpload = (
<DragContent
icon={<IconUpload className={s.icon} />}
title={I18n.t('upload_image_guide')}
tip={
<Popover
position="top"
content={
<div className="p-4 w-[240px]">
<div className="coz-fg-plus font-semi">
{I18n.t('bgi_upload_image_format_requirement_title')}
</div>
<div className="coz-fg-secondary text-xs">
{I18n.t('bgi_upload_image_format_requirement')}
</div>
<img src={IMG_SIZE_TIP} className="w-full mt-3" />
</div>
}
>
<Button
icon={<IconInfo />}
color="primary"
size={'mini'}
className={'!bg-transparent'}
/>
</Popover>
}
desc={I18n.t('upload_image_format_requirement')}
btnText={I18n.t('upload_image')}
btnOnClick={manualUpload}
/>
);
return mode === 'init' ? (
<div
className={`w-full flex items-center flex-col p-16 border border-dashed rounded-[6px] h-[466px] mb-6 ${
dragIn
? 'coz-stroke-hglt coz-bg-primary'
: 'coz-stroke-primary coz-bg-max'
}`}
onClick={e => e.stopPropagation()}
onDragEnter={() => {
setDragIn(true);
}}
onDragLeave={() => {
setDragIn(false);
}}
>
{customUpload}
{renderEnhancedUpload?.()}
</div>
) : (
<div className="opacity-80 coz-bg-max w-full h-full flex items-center flex-col justify-center">
{customUpload}
</div>
);
};

View File

@@ -0,0 +1,187 @@
/*
* 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 ReactCropperElement } from 'react-cropper';
import React, { useState, type RefObject, type ReactNode } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useGenerateImageStore } from '@coze-studio/bot-detail-store';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import 'cropperjs/dist/cropper.css';
import { type FileItem, type UploadProps } from '@coze-arch/bot-semi/Upload';
import { Toast, Upload } from '@coze-arch/bot-semi';
import { type BackgroundImageInfo } from '@coze-arch/bot-api/developer_api';
import {
useDragImage,
checkImageWidthAndHeight,
UploadMode,
MAX_IMG_SIZE,
} from '@coze-agent-ide/chat-background-shared';
import { DragUploadContent } from './drag-upload-content';
import CropperImg from './cropper';
export const checkHasFileOnDrag = (e: React.DragEvent<HTMLDivElement>) =>
// 判断的依据直接看 types 的类型解释就好了
Boolean(e.dataTransfer?.types.includes('Files'));
export interface CropperUploadProps {
pictureValue?: Partial<FileItem>;
onChange?: (value: FileItem) => void;
disabled?: boolean;
uploaderRef?: RefObject<Upload>;
cropperWebRef: RefObject<ReactCropperElement>;
cropperMobileRef: RefObject<ReactCropperElement>;
backgroundValue: BackgroundImageInfo[];
getUserId: () => string;
setUploadMode: (mode: UploadMode) => void;
uploadMode?: UploadMode;
renderEnhancedUpload?: (params: { aiUpload?: () => void }) => ReactNode;
}
const CopperUpload: React.FC<CropperUploadProps> = ({
pictureValue,
onChange,
disabled,
uploaderRef,
cropperWebRef,
cropperMobileRef,
backgroundValue = [],
getUserId,
setUploadMode,
uploadMode,
renderEnhancedUpload,
}) => {
const [loading, setLoading] = useState(false);
const mobileBackgroundInfo = backgroundValue[0]?.mobile_background_image;
const webBackgroundInfo = backgroundValue[0]?.web_background_image;
const { onDragEnter, onDragEnd, isDragIn, onDragOver } = useDragImage();
const { setGenerateBackgroundModalByImmer } = useGenerateImageStore(
useShallow(state => ({
setGenerateBackgroundModalByImmer:
state.setGenerateBackgroundModalByImmer,
})),
);
const customRequest: UploadProps['customRequest'] = async options => {
setLoading(true);
const { onSuccess, onError, file } = options;
if (typeof file === 'string') {
return;
}
try {
const { fileInstance } = file;
if (fileInstance) {
const validateSize = await checkImageWidthAndHeight(fileInstance);
if (validateSize) {
onSuccess(file);
onChange?.(file);
// 手动上传后 需要把候选图的选中态清掉
setGenerateBackgroundModalByImmer(state => {
state.selectedImage = {};
});
} else {
setLoading(false);
}
} else {
onError({
status: 0,
});
}
} catch (e) {
setLoading(false);
e instanceof Error &&
logger.error({ error: e, eventName: 'poll_scene_mockset_fail' });
}
};
const commonProps = {
url: pictureValue?.url,
loading,
setLoading,
getUserId,
};
const handleAIUpload = () => {
setUploadMode(UploadMode.Generate);
};
return (
<div className="px-6 mt-[1px]">
<Upload
action=""
limit={1}
draggable
customRequest={customRequest}
accept=".jpeg,.jpg,.png,.webp,.gif"
showReplace={false}
showUploadList={false}
ref={uploaderRef}
disabled={disabled}
maxSize={MAX_IMG_SIZE}
onDrop={onDragEnd}
onSizeError={() => {
Toast.error({
content: I18n.t('upload_image_size_limit', { max_size: '10M' }),
showClose: false,
});
}}
>
{pictureValue?.url || uploadMode === UploadMode.Generate ? (
<div
className="relative flex justify-between gap-3"
onClick={e => {
e.stopPropagation();
}}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
onDragLeave={onDragEnd}
>
<CropperImg
mode={'pc'}
cropperRef={cropperWebRef}
backgroundInfo={webBackgroundInfo}
{...commonProps}
/>
<CropperImg
mode={'mobile'}
cropperRef={cropperMobileRef}
backgroundInfo={mobileBackgroundInfo}
{...commonProps}
/>
{isDragIn ? (
<div className="w-full h-full absolute z-[300]">
<DragUploadContent mode="fill" />
</div>
) : null}
</div>
) : (
<DragUploadContent
manualUpload={() => {
setUploadMode(UploadMode.Manual);
uploaderRef?.current?.openFileDialog();
}}
renderEnhancedUpload={() =>
renderEnhancedUpload?.({ aiUpload: handleAIUpload })
}
/>
)}
</Upload>
</div>
);
};
export default CopperUpload;

View File

@@ -0,0 +1,191 @@
/*
* 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 ReactCropperElement } from 'react-cropper';
import React, { createRef, useEffect, useRef, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
DotStatus,
useGenerateImageStore,
} from '@coze-studio/bot-detail-store';
import { type FileItem } from '@coze-arch/bot-semi/Upload';
import { type Upload } from '@coze-arch/bot-semi';
import { type BackgroundImageInfo } from '@coze-arch/bot-api/developer_api';
import { AuditErrorMessage } from '@coze-studio/bot-audit-adapter';
import {
getInitBackground,
UploadMode,
useBackgroundContent,
useSubmitCroppedImage,
} from '@coze-agent-ide/chat-background-shared';
import CropperUpload, { type CropperUploadProps } from './cropper-upload';
import { CropperFooter } from './cropper-footer';
type PictureOnchangeCallback = (value: Partial<FileItem>) => void;
export interface BackgroundConfigContentProps
extends Pick<CropperUploadProps, 'renderEnhancedUpload'> {
onSuccess: (value: BackgroundImageInfo[]) => void;
backgroundValue: BackgroundImageInfo[];
getUserId: () => {
userId: string;
};
cancel: () => void;
renderUploadSlot?: (params: {
pictureOnChange: PictureOnchangeCallback;
pictureUrl: string | undefined;
uploadMode: UploadMode;
}) => React.ReactNode;
}
export const BackgroundConfigContent: React.FC<
BackgroundConfigContentProps
> = ({
backgroundValue = [],
getUserId,
onSuccess,
cancel,
renderUploadSlot,
renderEnhancedUpload,
}) => {
const uploaderRef = useRef<Upload>(null);
const cropperWebRef = createRef<ReactCropperElement>();
const cropperMobileRef = createRef<ReactCropperElement>();
const [loading, setLoading] = useState(false);
const [auditNotPass, setAuditNotPass] = useState(false);
const {
selectedImageInfo,
isGenerateSuccess,
setGenerateBackgroundModalByImmer,
} = useGenerateImageStore(
useShallow(state => ({
selectedImageInfo: state.generateBackGroundModal.selectedImage?.img_info,
isGenerateSuccess:
state.generateBackGroundModal.gif.dotStatus === DotStatus.Success ||
state.generateBackGroundModal.image.dotStatus === DotStatus.Success,
setGenerateBackgroundModalByImmer:
state.setGenerateBackgroundModalByImmer,
})),
);
// 初始化展示在拖拽框的图: AI生成成功的展示生成的 > 历史设置的背景图 > 空
const initPicture = getInitBackground({
isGenerateSuccess,
originBackground: backgroundValue,
selectedImageInfo,
});
const { showDot } = useBackgroundContent();
const initUploadMode =
showDot || initPicture.url ? UploadMode.Generate : UploadMode.Manual;
const [uploadMode, setUploadMode] = useState<UploadMode>(initUploadMode);
const [pictureValue, setPictureValue] =
useState<Partial<FileItem>>(initPicture);
const pictureUrl = pictureValue?.url;
useEffect(() => {
// 初始化逻辑: 初始化图 不是 选中的图,更新候选图选中态
if (initPicture.url !== selectedImageInfo?.tar_url) {
setGenerateBackgroundModalByImmer(state => {
state.selectedImage = {
img_info: {
tar_uri: initPicture.uri,
tar_url: initPicture.url,
},
};
});
}
}, []);
useEffect(() => {
// 收到AI生图成功后更新当前展示在裁剪框的图片
if (selectedImageInfo) {
setPictureValue({
uri: selectedImageInfo?.tar_uri,
url: selectedImageInfo?.tar_url,
});
setAuditNotPass(false);
}
}, [selectedImageInfo?.tar_url]);
const handleCancel = () => {
cancel();
clearAllSideEffect();
};
const clearAllSideEffect = () => {
[cropperWebRef, cropperMobileRef].forEach(item => {
item.current?.cropper?.destroy();
});
};
const { handleSubmit } = useSubmitCroppedImage({
onSuccess,
handleCancel,
cropperWebRef,
cropperMobileRef,
currentOriginImage: pictureValue ?? {},
setLoading,
getUserId,
onAuditCheck(notPass) {
setAuditNotPass(notPass);
},
});
const pictureOnChange: PictureOnchangeCallback = value => {
setPictureValue(value);
setAuditNotPass(false);
};
return (
<React.Fragment>
<div className="flex flex-col overflow-hidden flex-auto relative">
<CropperUpload
pictureValue={pictureValue}
onChange={pictureOnChange}
uploaderRef={uploaderRef}
cropperWebRef={cropperWebRef}
cropperMobileRef={cropperMobileRef}
backgroundValue={backgroundValue}
getUserId={() => getUserId().userId}
setUploadMode={setUploadMode}
uploadMode={uploadMode}
renderEnhancedUpload={renderEnhancedUpload}
/>
{renderUploadSlot?.({ pictureOnChange, pictureUrl, uploadMode })}
</div>
{pictureUrl || uploadMode === UploadMode.Generate ? (
<CropperFooter
handleOpenFileDialog={() => uploaderRef.current?.openFileDialog()}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
loading={loading}
disabledConfig={{
upload: !pictureUrl && uploadMode === UploadMode.Manual,
submit: !pictureUrl,
}}
/>
) : null}
{auditNotPass ? (
<div className="pb-6 coz-bg-max px-6">
<AuditErrorMessage />
</div>
) : null}
</React.Fragment>
);
};

View File

@@ -0,0 +1,21 @@
/*
* 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 {
BackgroundConfigContent,
type BackgroundConfigContentProps,
} from './components/chat-background-config-content';
export { DragContent } from './components/chat-background-config-content/cropper-upload/drag-upload-content';

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
/// <reference types='@coze-arch/bot-typings' />