feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
.text {
|
||||
font-size: 8px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
text-shadow: 0 0.5px 1px rgba(0, 0, 0, 20%);
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
.icon {
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
// todo token颜色
|
||||
color: #4E40E5;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
17
frontend/packages/agent-ide/chat-background-config-content/src/typings.d.ts
vendored
Normal file
17
frontend/packages/agent-ide/chat-background-config-content/src/typings.d.ts
vendored
Normal 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' />
|
||||
Reference in New Issue
Block a user