feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 569 B |
@@ -0,0 +1,176 @@
|
||||
.generate-list-wrap {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
|
||||
.hidden-element {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-image: url('../assets/bot-generate-loading-sprite.png'), url('../assets/bot-generate-dis-sprite.png')
|
||||
}
|
||||
|
||||
.split-line{
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 12px;
|
||||
background-color: var(--coz-stroke-plus);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
|
||||
&.checked::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background: url('../assets/list-checked-bold.png') no-repeat center center/cover;
|
||||
}
|
||||
|
||||
.loading-mask {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
|
||||
&.loading {
|
||||
background-image: url('../assets/bot-generate-loading-sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: -2px;
|
||||
background-size: 2190px 73px;
|
||||
|
||||
animation: loading 1.5s steps(30) infinite;
|
||||
}
|
||||
|
||||
&.finish {
|
||||
background-image: url('../assets/bot-generate-dis-sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: -2px;
|
||||
background-size: 730px 73px;
|
||||
|
||||
animation: finish .5s steps(10) forwards;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: #FFF;
|
||||
|
||||
background-color: var(--coz-mg-mask);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
filter: brightness(1.5) blur(6px);
|
||||
mix-blend-mode: hard-light;
|
||||
animation: fade-in .8s .2s forwards;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
|
||||
font-size: 12px;
|
||||
color: #4D53E8;
|
||||
|
||||
background-color: var(--coz-mg-primary);
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #B4BAF6;
|
||||
|
||||
svg {
|
||||
opacity: .4;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from {
|
||||
background-position: -2px -2px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -2192px -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes finish {
|
||||
from {
|
||||
background-position: -2px -2px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -730px -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
filter: brightness(1.5) blur(6px);
|
||||
mix-blend-mode: hard-light;
|
||||
}
|
||||
|
||||
30% {
|
||||
mix-blend-mode: unset;
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: brightness(1) blur(0);
|
||||
mix-blend-mode: unset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { isFunction } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import axios, { type CancelTokenSource } from 'axios';
|
||||
import { useHover } from 'ahooks';
|
||||
import {
|
||||
REPORT_EVENTS as ReportEventNames,
|
||||
createReportEvent,
|
||||
} from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCheckMark,
|
||||
IconCozCrossCircle,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Tooltip,
|
||||
Toast,
|
||||
Image,
|
||||
AIButton,
|
||||
Space,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { loadImage } from '@coze-arch/bot-utils';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
type UploadValue = { uid: string; url: string }[];
|
||||
interface GenerateInfo {
|
||||
name: string;
|
||||
desc?: string;
|
||||
}
|
||||
interface AutoGenerateProps {
|
||||
onChange: (value?: UploadValue) => void;
|
||||
generateInfo?: GenerateInfo | (() => GenerateInfo);
|
||||
generateTooltip?: {
|
||||
generateBtnText?: string;
|
||||
contentNotLegalText?: string;
|
||||
};
|
||||
showAiAvatar: boolean;
|
||||
/**
|
||||
* 最多允许多少个候选
|
||||
* @default 5
|
||||
*/
|
||||
maxCandidateCount?: number;
|
||||
}
|
||||
|
||||
interface PictureItem {
|
||||
url: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
// 自动生成头像错误码
|
||||
enum ErrorCode {
|
||||
OVER_QUOTA_PER_DAY = 700012034,
|
||||
CONTENT_NOT_LEGAL = 700012050,
|
||||
}
|
||||
|
||||
const MAX_CANDIDATE_COUNT = 5;
|
||||
const MAX_TOTAL_COUNT = 25;
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const AutoGenerate = (props: AutoGenerateProps) => {
|
||||
const {
|
||||
onChange,
|
||||
generateInfo,
|
||||
showAiAvatar,
|
||||
generateTooltip,
|
||||
maxCandidateCount = MAX_CANDIDATE_COUNT,
|
||||
} = props;
|
||||
const cancelTokenSource = useRef<CancelTokenSource>();
|
||||
const hoverCount = useRef(0);
|
||||
const loadingRef = useRef<HTMLDivElement>(null);
|
||||
const loadingHover = useHover(loadingRef);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [showGenerateBtn, setShowGenerateBtn] = useState(true);
|
||||
const [pictureList, setPictureList] = useState<PictureItem[]>([]);
|
||||
const [checkedId, setCheckedId] = useState(-1);
|
||||
|
||||
const tooltipContent = useMemo(() => {
|
||||
if (totalCount >= MAX_TOTAL_COUNT && pictureList.length === 0) {
|
||||
return I18n.t('bot_edit_profile_pircture_autogen_quota_tooltip');
|
||||
}
|
||||
|
||||
const defaultText = IS_OVERSEA
|
||||
? I18n.t('bot_edit_profile_pircture_autogen_tooltip')
|
||||
: I18n.t('bot_edit_profile_pircture_autogen_tooltip_cn');
|
||||
return generateTooltip?.generateBtnText || defaultText;
|
||||
}, [totalCount, pictureList.length, generateTooltip?.generateBtnText]);
|
||||
|
||||
const allowGenerate = useMemo(
|
||||
() =>
|
||||
totalCount < MAX_TOTAL_COUNT &&
|
||||
(isFunction(generateInfo) ? generateInfo?.().name : generateInfo?.name),
|
||||
[generateInfo, totalCount],
|
||||
);
|
||||
|
||||
const cancelGenerate = () => {
|
||||
cancelTokenSource.current?.cancel('cancel generate picture');
|
||||
};
|
||||
|
||||
const updateParentValue = (id: number, value: UploadValue) => {
|
||||
setCheckedId(id);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const getPicture = async () => {
|
||||
hoverCount.current = 1;
|
||||
setLoading(true);
|
||||
|
||||
const newList = [
|
||||
...pictureList,
|
||||
{
|
||||
url: '',
|
||||
uid: '',
|
||||
},
|
||||
];
|
||||
setPictureList(newList);
|
||||
const reportEvent = createReportEvent({
|
||||
eventName: ReportEventNames.botGetAiGenerateAvatar,
|
||||
});
|
||||
try {
|
||||
cancelTokenSource.current = axios.CancelToken.source();
|
||||
const { name, desc } = isFunction(generateInfo)
|
||||
? generateInfo()
|
||||
: generateInfo || {};
|
||||
const { data } = await DeveloperApi.GenerateIcon(
|
||||
{
|
||||
bot_name: name,
|
||||
description: desc,
|
||||
},
|
||||
{
|
||||
__disableErrorToast: true,
|
||||
cancelToken: cancelTokenSource.current.token,
|
||||
},
|
||||
);
|
||||
setTotalCount(Number(data?.count));
|
||||
await loadImage(String(data?.icon_url));
|
||||
setLoading(false);
|
||||
setPictureList(prevList => {
|
||||
prevList[prevList.length - 1] = {
|
||||
url: String(data?.icon_url),
|
||||
uid: String(data?.icon_uri),
|
||||
};
|
||||
return prevList;
|
||||
});
|
||||
updateParentValue(newList.length - 1, [
|
||||
{
|
||||
url: String(data?.icon_url),
|
||||
uid: String(data?.icon_uri),
|
||||
},
|
||||
]);
|
||||
reportEvent.success();
|
||||
} catch (error) {
|
||||
onChange();
|
||||
setLoading(false);
|
||||
setPictureList(list => {
|
||||
list.pop();
|
||||
return list;
|
||||
});
|
||||
const codeNumber = Number((error as { code: number })?.code);
|
||||
if (codeNumber === ErrorCode.OVER_QUOTA_PER_DAY) {
|
||||
// 超过单日次数上限
|
||||
setTotalCount(MAX_TOTAL_COUNT);
|
||||
Toast.error({
|
||||
content: I18n.t('bot_edit_profile_pircture_autogen_quota_tooltip'),
|
||||
showClose: false,
|
||||
});
|
||||
reportEvent.error({
|
||||
reason: 'The number of times in a day exceeded the upper limit',
|
||||
error: error instanceof Error ? error : void 0,
|
||||
});
|
||||
} else if (codeNumber === ErrorCode.CONTENT_NOT_LEGAL) {
|
||||
Toast.error({
|
||||
content:
|
||||
generateTooltip?.contentNotLegalText ||
|
||||
I18n.t('generate_bot_icon_content_filter'),
|
||||
showClose: false,
|
||||
});
|
||||
reportEvent.error({
|
||||
reason:
|
||||
"The bot's name or description contains inappropriate content",
|
||||
error: error instanceof Error ? error : void 0,
|
||||
});
|
||||
} else if (codeNumber > 0) {
|
||||
Toast.error({
|
||||
content: I18n.t('error'),
|
||||
showClose: false,
|
||||
});
|
||||
reportEvent.error({
|
||||
reason: 'Failed to generate profile picture',
|
||||
error: error instanceof Error ? error : void 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 获取当日总生成次数
|
||||
DeveloperApi.GetGenerateIconInfo()
|
||||
.then(({ data }) => {
|
||||
setTotalCount(Number(data?.current_day_count));
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
pictureList.length >= maxCandidateCount ||
|
||||
(totalCount >= MAX_TOTAL_COUNT && pictureList.length > 0)
|
||||
) {
|
||||
setShowGenerateBtn(false);
|
||||
} else {
|
||||
setShowGenerateBtn(true);
|
||||
}
|
||||
}, [pictureList.length, totalCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingRef.current) {
|
||||
hoverCount.current++;
|
||||
}
|
||||
}, [loadingHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAiAvatar) {
|
||||
setCheckedId(-1);
|
||||
if (loading) {
|
||||
cancelGenerate();
|
||||
}
|
||||
}
|
||||
}, [showAiAvatar]);
|
||||
|
||||
return (
|
||||
<div className={s['generate-list-wrap']}>
|
||||
<div className={s['hidden-element']} />
|
||||
<div className={s['split-line']} />
|
||||
<Space spacing={4}>
|
||||
{(pictureList || []).map((picture, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={classNames(s.avatar)}
|
||||
onClick={() => {
|
||||
if (picture.url) {
|
||||
updateParentValue(idx, [
|
||||
{ url: picture.url, uid: String(picture.uid) },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={loadingRef}
|
||||
className={classNames(s['loading-mask'], {
|
||||
[s.loading]: !picture.url,
|
||||
[s.finish]: picture.url,
|
||||
[s['loading-hover']]: loadingHover && !picture.url,
|
||||
})}
|
||||
>
|
||||
{/* 二次hover展示取消 */}
|
||||
{hoverCount.current > 1 && loadingHover && !picture.url ? (
|
||||
<div
|
||||
className={s.mask}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
cancelGenerate();
|
||||
}}
|
||||
>
|
||||
<IconCozCrossCircle />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 选中图片蒙版 */}
|
||||
{checkedId === idx && (
|
||||
<div className={s.mask}>
|
||||
<IconCozCheckMark className="text-[16]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Image
|
||||
className={picture.url && s['avatar-img']}
|
||||
preview={false}
|
||||
src={picture.url}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{showGenerateBtn ? (
|
||||
<Tooltip position="topLeft" content={tooltipContent}>
|
||||
<AIButton
|
||||
onlyIcon
|
||||
color="aihglt"
|
||||
disabled={!allowGenerate}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
if (allowGenerate) {
|
||||
getPicture();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
.upload {
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
height: 64px;
|
||||
margin: auto;
|
||||
|
||||
.circle {
|
||||
:global {
|
||||
.semi-upload-picture-file-card-uploading::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 14px;
|
||||
|
||||
>img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-skeleton-image {
|
||||
background-color: #fff;
|
||||
border: 1px solid rgb(29 28 35 / 8%);
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-button-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
.mask {
|
||||
cursor: pointer;
|
||||
|
||||
&.full-center {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: rgba(255, 255, 255, 0%);
|
||||
|
||||
visibility: hidden;
|
||||
background-color: rgba(22, 22, 26, 0%);
|
||||
border-radius: 14px;
|
||||
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
&.right-bottom {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 4px 0 0 4px;
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mask {
|
||||
&.full-center {
|
||||
color: #fff;
|
||||
visibility: visible;
|
||||
background-color: var(--coz-mg-mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-with-auto-generate {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.upload {
|
||||
height: 64px;
|
||||
margin: 0;
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 {
|
||||
PictureUpload,
|
||||
type GenerateInfo,
|
||||
type UploadValue,
|
||||
} from './picture-upload';
|
||||
export { default as customUploadRequest } from './utils/custom-upload-request';
|
||||
export { type RenderAutoGenerateParams } from './picture-upload';
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { useMemo, useRef, useState, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useMount } from 'ahooks';
|
||||
import { CommonE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozEdit } from '@coze-arch/coze-design/icons';
|
||||
import { type FileItem, type UploadProps } from '@coze-arch/bot-semi/Upload';
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import { UIButton, Toast, withField, Image, Upload } from '@coze-arch/bot-semi';
|
||||
import { IconAvatarEditMask } from '@coze-arch/bot-icons';
|
||||
import { type FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import customUploadRequest from './utils/custom-upload-request';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export type UploadValue = { uid: string | undefined; url: string }[];
|
||||
export interface GenerateInfo {
|
||||
name: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
export interface RenderAutoGenerateParams {
|
||||
uploadPicture: () => void;
|
||||
showAiAvatar: boolean;
|
||||
setShowAiAvatar: (show: boolean) => void;
|
||||
generateInfo?: GenerateInfo | (() => GenerateInfo);
|
||||
generateTooltip?: {
|
||||
generateBtnText?: string;
|
||||
contentNotLegalText?: string;
|
||||
};
|
||||
onChange?: (value: UploadValue) => void;
|
||||
maxCandidateCount?: number;
|
||||
}
|
||||
interface PackageUploadProps {
|
||||
value?: FileItem[];
|
||||
onChange?: (value: UploadValue) => void;
|
||||
fileBizType: FileBizType;
|
||||
uploadButtonText?: string;
|
||||
iconType?: IconType;
|
||||
disabled?: boolean;
|
||||
avatarClassName?: string;
|
||||
uploadClassName?: string;
|
||||
triggerClassName?: string;
|
||||
maskIcon?: React.ReactNode;
|
||||
/**
|
||||
* 编辑遮罩的展示模式
|
||||
* - full-center(默认): 整体覆盖黑色透明遮罩, Icon 居中展示. hover 展示
|
||||
* - right-bottom: 右下角遮罩, 长期展示
|
||||
*/
|
||||
maskMode?: 'full-center' | 'right-bottom';
|
||||
/** 编辑遮罩的 className */
|
||||
editMaskClassName?: string;
|
||||
/** max size */
|
||||
maxSize?: number;
|
||||
withAutoGenerate?: boolean;
|
||||
generateInfo?: GenerateInfo | (() => GenerateInfo);
|
||||
generateTooltip?: {
|
||||
generateBtnText?: string;
|
||||
contentNotLegalText?: string;
|
||||
};
|
||||
/**
|
||||
* 自动生成的最大候选数量
|
||||
* @default 5
|
||||
*/
|
||||
maxCandidateCount?: number;
|
||||
beforeUploadCustom?: () => void;
|
||||
afterUploadCustom?: () => void;
|
||||
accept?: string;
|
||||
onGenerateStaticImageClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onGenerateGifClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onSizeError?: () => void;
|
||||
// 自定义自定生成图片逻辑
|
||||
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
const _PictureUpload = (props: PackageUploadProps) => {
|
||||
// 业务
|
||||
const {
|
||||
onChange,
|
||||
value,
|
||||
fileBizType,
|
||||
uploadButtonText,
|
||||
iconType = IconType.Bot,
|
||||
disabled = false,
|
||||
avatarClassName,
|
||||
uploadClassName,
|
||||
triggerClassName,
|
||||
maskIcon,
|
||||
maskMode = 'full-center',
|
||||
editMaskClassName,
|
||||
withAutoGenerate = false,
|
||||
generateInfo,
|
||||
generateTooltip,
|
||||
beforeUploadCustom,
|
||||
afterUploadCustom,
|
||||
accept = 'image/*',
|
||||
maxCandidateCount,
|
||||
renderAutoGenerate,
|
||||
onSizeError,
|
||||
maxSize = 2 * 1024,
|
||||
testId,
|
||||
} = props;
|
||||
const uploadRef = useRef<Upload>(null);
|
||||
const pictureValue = value?.at(0);
|
||||
const [loadingIcon, setLoadingIcon] = useState(!pictureValue);
|
||||
const [showAiAvatar, setShowAiAvatar] = useState(withAutoGenerate);
|
||||
const maskIconInner = useMemo(() => {
|
||||
if (maskIcon) {
|
||||
return maskIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconCozEdit
|
||||
className={classNames(
|
||||
maskMode === 'right-bottom' ? 'text-[14px]' : 'text-[24px]',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}, [maskIcon, maskMode]);
|
||||
|
||||
const getIcon = async () => {
|
||||
setLoadingIcon(true);
|
||||
try {
|
||||
const res = await DeveloperApi.GetIcon({
|
||||
icon_type: iconType,
|
||||
});
|
||||
const iconData = res.data?.icon_list?.[0];
|
||||
if (!iconData) {
|
||||
Toast.error({
|
||||
content: I18n.t('error'),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { url = '', uri = '' } = iconData;
|
||||
onChange?.([
|
||||
{
|
||||
url,
|
||||
uid: uri,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
Toast.error({
|
||||
content: I18n.t('error'),
|
||||
showClose: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useMount(() => {
|
||||
if (!pictureValue) {
|
||||
getIcon().then(() => setLoadingIcon(false));
|
||||
}
|
||||
});
|
||||
|
||||
const customRequest: UploadProps['customRequest'] = options => {
|
||||
customUploadRequest({
|
||||
...options,
|
||||
fileBizType,
|
||||
onSuccess: data => {
|
||||
if (withAutoGenerate) {
|
||||
setShowAiAvatar(false);
|
||||
}
|
||||
options.onSuccess(data);
|
||||
onChange?.([
|
||||
{
|
||||
uid: data?.upload_uri || '',
|
||||
url: data?.upload_url || '',
|
||||
},
|
||||
]);
|
||||
},
|
||||
beforeUploadCustom,
|
||||
afterUploadCustom,
|
||||
});
|
||||
};
|
||||
|
||||
const uploadPicture = () => {
|
||||
uploadRef.current?.openFileDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={withAutoGenerate ? s['upload-with-auto-generate'] : ''}
|
||||
data-testid={CommonE2e.PictureUpload}
|
||||
>
|
||||
<Upload
|
||||
action=""
|
||||
className={classNames(s.upload, uploadClassName)}
|
||||
limit={1}
|
||||
customRequest={customRequest}
|
||||
fileList={value}
|
||||
accept={accept}
|
||||
showReplace={false}
|
||||
showUploadList={false}
|
||||
ref={uploadRef}
|
||||
disabled={disabled}
|
||||
maxSize={maxSize}
|
||||
onSizeError={() => {
|
||||
if (onSizeError) {
|
||||
onSizeError();
|
||||
return;
|
||||
}
|
||||
Toast.error({
|
||||
// starling 切换
|
||||
content: I18n.t(
|
||||
'dataset_upload_image_warning',
|
||||
{},
|
||||
'Please upload an image less than 2MB',
|
||||
),
|
||||
showClose: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
s['avatar-wrap'],
|
||||
'cursor-pointer',
|
||||
triggerClassName,
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<Image
|
||||
preview={false}
|
||||
className={classNames(
|
||||
s.avatar,
|
||||
loadingIcon && s['avatar-loading'],
|
||||
avatarClassName,
|
||||
)}
|
||||
placeholder={
|
||||
<Image
|
||||
className={classNames(s.avatar, avatarClassName)}
|
||||
src={pictureValue?.url}
|
||||
preview={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className={classNames(s.mask, s[maskMode], editMaskClassName)}>
|
||||
{maskMode === 'right-bottom' && (
|
||||
<IconAvatarEditMask className="absolute inset-0 w-full h-full rounded-br-[14px] overflow-hidden" />
|
||||
)}
|
||||
<div className="relative inline-flex">{maskIconInner}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Upload>
|
||||
{uploadButtonText && !disabled ? (
|
||||
<div className={s['upload-button-wrap']}>
|
||||
<UIButton
|
||||
className={s['upload-button']}
|
||||
theme="borderless"
|
||||
type="primary"
|
||||
onClick={uploadPicture}
|
||||
>
|
||||
{uploadButtonText}
|
||||
</UIButton>
|
||||
</div>
|
||||
) : null}
|
||||
{withAutoGenerate && renderAutoGenerate
|
||||
? renderAutoGenerate({
|
||||
uploadPicture,
|
||||
showAiAvatar,
|
||||
setShowAiAvatar,
|
||||
generateInfo,
|
||||
generateTooltip,
|
||||
onChange,
|
||||
maxCandidateCount,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const PictureUpload: FC<CommonFieldProps & PackageUploadProps> =
|
||||
withField(_PictureUpload);
|
||||
@@ -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 { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type customRequestArgs } from '@coze-arch/bot-semi/Upload';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import {
|
||||
type UploadFileData,
|
||||
type FileBizType,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import getBase64 from './get-base64';
|
||||
|
||||
function customUploadRequest(
|
||||
options: Omit<customRequestArgs, 'onSuccess'> & {
|
||||
fileBizType: FileBizType;
|
||||
onSuccess: (data?: UploadFileData) => void;
|
||||
beforeUploadCustom?: () => void;
|
||||
afterUploadCustom?: () => void;
|
||||
},
|
||||
): void {
|
||||
const {
|
||||
onSuccess,
|
||||
onError,
|
||||
file,
|
||||
beforeUploadCustom,
|
||||
afterUploadCustom,
|
||||
fileBizType,
|
||||
} = options;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
return;
|
||||
}
|
||||
beforeUploadCustom?.();
|
||||
const getFileExtension = (name: string) => {
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1);
|
||||
};
|
||||
try {
|
||||
const { fileInstance } = file;
|
||||
|
||||
// 业务
|
||||
if (fileInstance) {
|
||||
const extension = getFileExtension(file.name);
|
||||
|
||||
// 业务
|
||||
(async () => {
|
||||
try {
|
||||
const base64 = await getBase64(fileInstance);
|
||||
const result = await DeveloperApi.UploadFile({
|
||||
file_head: {
|
||||
file_type: extension,
|
||||
biz_type: fileBizType,
|
||||
},
|
||||
data: base64,
|
||||
});
|
||||
onSuccess?.(result.data);
|
||||
afterUploadCustom?.();
|
||||
} catch (error) {
|
||||
// 如参数校验失败情况会走到catch
|
||||
afterUploadCustom?.();
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
afterUploadCustom?.();
|
||||
throw new CustomError(ReportEventNames.parmasValidation, I18n.t('error'));
|
||||
}
|
||||
} catch (e) {
|
||||
afterUploadCustom?.();
|
||||
onError?.({
|
||||
status: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default customUploadRequest;
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
function getBase64(file: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
if (!result || typeof result !== 'string') {
|
||||
reject(
|
||||
new CustomError(ReportEventNames.parmasValidation, 'file read fail'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(result.replace(/^.*?,/, ''));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export default getBase64;
|
||||
Reference in New Issue
Block a user