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

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,67 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { Modal, Form, Input, type ModalProps } from '@coze-arch/coze-design';
export interface DeleteProjectBaseProps {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
}
export interface DeleteProjectModalProps
extends Omit<
ModalProps,
'size' | 'footer' | 'header' | 'okButtonColor' | 'okText' | 'cancelText'
>,
DeleteProjectBaseProps {}
const DeleteProjectContent: React.FC<DeleteProjectBaseProps> = ({
value,
onChange,
placeholder,
}) => (
<>
<div className="coz-fg-secondary leading-20px text-[14px] font-normal mb-16px">
{I18n.t('project_ide_delete_confirm_describe')}
</div>
<Form.Label required>{I18n.t('project_ide_project_name')}</Form.Label>
<Input value={value} onChange={onChange} placeholder={placeholder} />
</>
);
export const DeleteProjectModal: React.FC<DeleteProjectModalProps> = ({
value,
onChange,
placeholder,
...restModalProps
}) => (
<Modal
size="default"
header={I18n.t('project_ide_delete_confirm')}
okButtonColor="red"
okText={I18n.t('project_ide_delete')}
cancelText={I18n.t('Cancel')}
{...restModalProps}
>
<DeleteProjectContent
value={value}
onChange={onChange}
placeholder={placeholder}
/>
</Modal>
);

View File

@@ -0,0 +1,37 @@
/* stylelint-disable declaration-no-important */
.guide-modal {
:global(.semi-modal-content) {
padding-right: 0 !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
}
:global(.semi-modal-header) {
padding-right: 24px;
padding-left: 24px;
}
:global(.semi-modal-footer) {
display: none;
}
}
.guide-button {
box-shadow: -8px -18px 30px 0 #DAE6F7 inset;
}
.guide-button-hover {
&:hover {
.guide-img-bg {
background: linear-gradient(29deg, rgba(79, 62, 255, 17%) 15.59%, rgba(242, 146, 255, 14%) 42.22%, rgba(242, 146, 255, 0%) 72.41%), var(--coz-bg-primary, #F8F8FC);
}
.guide-desc-hover {
opacity: 0;
}
.create-button-hover {
opacity: 1;
}
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import {
Badge,
Button,
Avatar,
Modal,
type ModalProps,
} from '@coze-arch/coze-design';
import { useHiddenSession } from '../../hooks/use-hidden-session';
import ProjectImg from '../../assets/project-img.png';
import ProjectImgOversea from '../../assets/project-img-oversea.png';
import AgentImg from '../../assets/agent-img.png';
import AgentImgOversea from '../../assets/agent-img-oversea.png';
import styles from './index.module.less';
export type CreateType = 'project' | 'agent';
export interface GuideModalProps
extends Omit<ModalProps, 'size' | 'footer' | 'header' | 'onCancel'> {
onCancel: () => void;
onChange: (type: CreateType) => void;
extraButtonConfigs?: GuideButtonProps[];
}
interface GuideButtonProps {
onClick: () => void;
assetSrc: string;
title: ReactNode;
description: ReactNode;
tip?: ReactNode;
}
export const GuideButton: React.FC<GuideButtonProps> = ({
onClick,
assetSrc,
title,
description,
tip,
}) => {
const { isSessionHidden, hideSession } = useHiddenSession(
tip ? 'guideTip' : '',
);
const showTip = !isSessionHidden && Boolean(tip);
return (
<div
onClick={onClick}
className={classNames(
'relative cursor-pointer p-8px pb-16px hover:coz-shadow-default coz-bg-max coz-stroke-primary border-[1px] border-solid rounded-[12px] flex flex-col items-center',
styles['guide-button-hover'],
)}
>
<Avatar
src={assetSrc}
className={classNames(
'w-[314px] h-[240px] rounded-[8px] coz-bg-secondary',
styles['guide-img-bg'],
styles['guide-button'],
{
'!mb-[-28px]': showTip,
},
)}
imgCls="w-full h-full"
bottomSlot={{
render: () =>
showTip ? (
<div className="z-10 px-2 coz-fg-hglt text-[12px] font-medium w-full flex justify-center items-center h-[28px] rounded-[4px] rounded-t-none bg-[#DEDBFF]">
<div className="mx-auto">{tip}</div>
<IconCozCross
className="w-[12px] h-[12px]"
onClick={e => {
e.stopPropagation();
hideSession();
}}
/>
</div>
) : null,
text: tip,
textColor: '',
bgColor: '#DEDBFF',
className: '',
}}
/>
<div className="mb-[4px] mt-[20px] coz-fg-plus text-[20px] font-medium leading-[28px]">
{title}
</div>
<div
className={classNames(
'mb-[8px] coz-fg-secondary text-[14px] font-normal leading-[20px] opacity-100',
styles['guide-desc-hover'],
)}
>
{description}
</div>
<div
className={classNames(
'absolute w-full flex justify-center left-0 bottom-[12px] opacity-0',
styles['create-button-hover'],
)}
>
<Button>{I18n.t('create_title')}</Button>
</div>
</div>
);
};
const ProjectAsset = IS_OVERSEA ? ProjectImgOversea : ProjectImg;
const AgentAsset = IS_OVERSEA ? AgentImgOversea : AgentImg;
export const GuideModal: React.FC<GuideModalProps> = ({
onChange,
extraButtonConfigs = [],
...modalProps
}) => (
<Modal
// 清除 modal 自带边距 由内部 padding 撑开 展示按钮阴影
className={styles['guide-modal']}
size="xl"
title={I18n.t('create_title')}
width={'fit-content'}
{...modalProps}
>
<div className="flex justify-between pl-24px pb-24px pr-24px gap-[8px]">
<GuideButton
onClick={() => onChange('agent')}
assetSrc={AgentAsset}
title={I18n.t('creat_project_creat_agent')}
description={I18n.t('creat_project_agent_describe')}
tip={!IS_OPEN_SOURCE ? I18n.t('agent_creat_tips') : null}
/>
<GuideButton
onClick={() => onChange('project')}
assetSrc={ProjectAsset}
title={
<span className="flex gap-x-4px items-center">
{I18n.t('creat_project_creat_project')}
<Badge count="Beta" type="alt" />
</span>
}
description={
IS_OPEN_SOURCE
? I18n.t('creat_project_describe_open')
: I18n.t('creat_project_describe')
}
/>
{extraButtonConfigs.map(({ onClick, ...config }, index) => (
<GuideButton
key={index}
onClick={() => {
modalProps.onCancel();
onClick();
}}
{...config}
/>
))}
</div>
</Modal>
);
GuideModal.displayName = 'GuideModal';

View File

@@ -0,0 +1,135 @@
/*
* 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 { useRef, useState, Suspense, lazy } from 'react';
import { SpaceFormSelect } from '@coze-studio/components';
import { type AuditData } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { type RenderAutoGenerateParams } from '@coze-common/biz-components/picture-upload';
import { type FormApi, Modal, type ModalProps } from '@coze-arch/coze-design';
import {
filedKeyMap,
ProjectForm,
type ProjectFormProps,
type ProjectFormValues,
ProjectInfoFieldFragment,
} from '../project-form';
import { useFormSubmitState } from '../../hooks/use-project-form-submit-state';
const LazyReactMarkdown = lazy(() => import('react-markdown'));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ReactMarkdown = (props: any) => (
<Suspense fallback={null}>
<LazyReactMarkdown {...props} />
</Suspense>
);
interface ProjectFormModalProps
extends Omit<
ModalProps,
'size' | 'okText' | 'cancelText' | 'okButtonProps' | 'onOk'
> {
/** @default false */
showMonetizeConfig?: boolean;
selectSpace?: boolean;
formProps?: Omit<ProjectFormProps, 'getFormApi' | 'onValueChange'>;
request: (param: ProjectFormValues) => Promise<AuditData>;
isFormValid: (values: ProjectFormValues) => boolean;
}
export type BizProjectFormModalProps = ProjectFormModalProps & {
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
};
export const ProjectFormModal: React.FC<BizProjectFormModalProps> = ({
selectSpace,
formProps = {},
isFormValid,
request,
showMonetizeConfig,
renderAutoGenerate,
...restModalProps
}) => {
const [loading, setLoading] = useState(false);
const [auditResult, setAuditResult] = useState<AuditData>({
check_not_pass: false,
});
const {
bizCallback: { onAfterUpload, onBeforeUpload, onValuesChange },
isSubmitDisabled,
} = useFormSubmitState<ProjectFormValues>({
initialValues: formProps.initValues,
getIsFormValid: isFormValid,
});
const formApi = useRef<FormApi<ProjectFormValues>>();
const onFormSubmit: ModalProps['onOk'] = async () => {
if (!formApi.current) {
return;
}
try {
setLoading(true);
const auditData = await request(formApi.current.getValues());
setAuditResult(auditData);
// 没有通过校验就不关闭弹窗
if (auditData.check_not_pass) {
return;
}
} finally {
setLoading(false);
}
};
return (
<Modal
size="default"
okText={I18n.t('Confirm')}
cancelText={I18n.t('Cancel')}
okButtonProps={{
disabled: isSubmitDisabled,
loading,
}}
onOk={onFormSubmit}
{...restModalProps}
>
<ProjectForm
{...formProps}
getFormApi={api => {
formApi.current = api;
}}
onValueChange={onValuesChange}
>
{selectSpace ? <SpaceFormSelect field={filedKeyMap.space_id} /> : null}
<ProjectInfoFieldFragment
showMonetizeConfig={showMonetizeConfig}
onBeforeUpload={onBeforeUpload}
onAfterUpload={onAfterUpload}
renderAutoGenerate={renderAutoGenerate}
/>
</ProjectForm>
{auditResult.check_not_pass ? (
<div className="coz-fg-hglt-red mt-[-8px]">
<ReactMarkdown skipHtml={true} linkTarget="_blank">
{/* 注意使用 || msg undefined 或者空字符串都走兜底 */}
{auditResult.check_not_pass_msg || I18n.t('publish_audit_pop7')}
</ReactMarkdown>
</div>
) : null}
</Modal>
);
};

View File

@@ -0,0 +1,143 @@
/*
* 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 PropsWithChildren } from 'react';
import {
type DraftProjectCopyRequest,
type DraftProjectUpdateRequest,
type DraftProjectCreateRequest,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
import {
PictureUpload,
type RenderAutoGenerateParams,
} from '@coze-common/biz-components/picture-upload';
import { botInputLengthService } from '@coze-agent-ide/bot-input-length-limit';
import { IconCozUpload } from '@coze-arch/coze-design/icons';
import {
type BaseFormProps,
Form,
FormInput,
FormTextArea,
useFormApi,
withField,
} from '@coze-arch/coze-design';
import { SwitchWithDesc } from '../switch-with-desc';
import { type ModifyUploadValueType } from '../../type';
export type ProjectFormValues = ModifyUploadValueType<
Omit<DraftProjectCreateRequest, 'monetization_conf' | 'create_from'> &
DraftProjectCopyRequest &
DraftProjectUpdateRequest & {
enableMonetize?: boolean;
}
>;
export type ProjectFormSubmitValues = DraftProjectCreateRequest;
export type ProjectFormProps = BaseFormProps<ProjectFormValues>;
export interface ProjectInfoFieldProps {
/** @default false */
showMonetizeConfig?: boolean;
onBeforeUpload?: () => void;
onAfterUpload?: () => void;
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
}
export const ProjectForm: React.FC<PropsWithChildren<ProjectFormProps>> = ({
children,
...formProps
}) => <Form<ProjectFormValues> {...formProps}>{children}</Form>;
export const filedKeyMap: Record<
keyof ProjectFormValues,
keyof ProjectFormValues
> = {
name: 'name',
enableMonetize: 'enableMonetize',
description: 'description',
icon_uri: 'icon_uri',
space_id: 'space_id',
project_id: 'project_id',
to_space_id: 'to_space_id',
} as const;
export const ProjectInfoFieldFragment: React.FC<ProjectInfoFieldProps> = ({
showMonetizeConfig,
onAfterUpload,
onBeforeUpload,
renderAutoGenerate,
}) => {
const formApi = useFormApi<ProjectFormValues>();
return (
<>
<FormInput
label={I18n.t('creat_project_project_name')}
rules={[{ required: true }]}
field={filedKeyMap.name}
maxLength={botInputLengthService.getInputLengthLimit('projectName')}
getValueLength={botInputLengthService.getValueLength}
noErrorMessage
/>
{showMonetizeConfig ? (
<FormSwitch
field={filedKeyMap.enableMonetize}
label={I18n.t('monetization')}
desc={I18n.t('monetization_des')}
initValue={true}
rules={[{ required: true }]}
/>
) : null}
<FormTextArea
label={I18n.t('creat_project_project_describe')}
field={filedKeyMap.description}
maxCount={botInputLengthService.getInputLengthLimit(
'projectDescription',
)}
maxLength={botInputLengthService.getInputLengthLimit(
'projectDescription',
)}
getValueLength={botInputLengthService.getValueLength}
/>
<PictureUpload
accept=".jpeg,.jpg,.png,.gif"
label={I18n.t('bot_edit_profile_pircture')}
field={filedKeyMap.icon_uri}
rules={[{ required: true }]}
fileBizType={FileBizType.BIZ_BOT_ICON}
iconType={IconType.Bot}
maskIcon={<IconCozUpload />}
withAutoGenerate
renderAutoGenerate={renderAutoGenerate}
generateInfo={() => {
const values = formApi.getValues();
return {
name: values?.name,
desc: values?.description,
};
}}
beforeUploadCustom={onBeforeUpload}
afterUploadCustom={onAfterUpload}
/>
</>
);
};
const FormSwitch = withField(SwitchWithDesc);

View File

@@ -0,0 +1,35 @@
/* stylelint-disable declaration-no-important */
.project-template-modal {
:global(.semi-modal) {
width: 960px !important;
}
:global(.semi-modal-content) {
padding-right: 0 !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
}
:global(.semi-modal-header) {
padding-right: 24px;
padding-left: 24px;
}
}
.error-empty {
padding: 124px 0;
:global {
.semi-empty-content {
margin-top: 8px;
}
.semi-empty-footer {
display: flex;
justify-content: center;
margin-top: 8px;
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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 { groupBy, xorBy } from 'lodash-es';
import { useRequest } from 'ahooks';
import {
ProductEntityType,
ProductListSource,
SortType,
type ProductInfo,
} from '@coze-arch/idl/product_api';
import { I18n } from '@coze-arch/i18n';
import { ProductApi } from '@coze-arch/bot-api';
import {
IllustrationFailure,
IllustrationFailureDark,
} from '@douyinfe/semi-illustrations';
import { IconCozRefresh } from '@coze-arch/coze-design/icons';
import { Button, Empty, Modal, type ModalProps } from '@coze-arch/coze-design';
import {
type BeforeProjectTemplateCopyCallback,
type ProjectTemplateCopySuccessCallback,
useProjectTemplateCopyModal,
} from '../../hooks/use-project-template-copy-modal';
import {
CardSkeleton,
TemplateGroupSkeleton,
} from './template-components/skeleton';
import { ProjectTemplateGroup } from './template-components/project-template-group';
import {
openTemplatePreview,
ProjectTemplateCard,
} from './template-components/project-template-card';
import { CreateEmptyProjectUI } from './template-components/create-empty-project-ui';
import styles from './index.module.less';
const MAX_PAGE_SIZE = 50;
export interface ProjectTemplateBaseProps {
spaceId?: string;
isSelectSpaceOnCopy: boolean;
onBeforeCopy: BeforeProjectTemplateCopyCallback | undefined;
onCopyError: (() => void) | undefined;
onCopyOk: ProjectTemplateCopySuccessCallback | undefined;
onCreateProject?: () => void;
}
/**
* 需要特别处理、放到「基础」类别中的模版
* 本身业务中没有「基础」这个类别,但是在这个通过复制创建的场景下 pm 希望为用户提供一些具有基本功能有代表性的模版
* 所以这个特殊处理的模版应运而生
*
* 迭代时需要注意,业务上需要保证这些模版都是被 recommend 才能复用 PublicGetProductList 这个接口
*/
const BASE_TEMPLATE_ID_LIST = ['7439261984903938074'];
const ProjectTemplateContent: React.FC<ProjectTemplateBaseProps> = ({
spaceId,
isSelectSpaceOnCopy,
onCopyOk,
onCreateProject,
onBeforeCopy,
onCopyError,
}) => {
const {
data: categories,
error: categoriesError,
loading: isCategoryLoading,
refresh: refreshCategoryRequest,
} = useRequest(async () => {
const response = await ProductApi.PublicGetProductCategoryList({
entity_type: ProductEntityType.TemplateCommon,
});
return response.data?.categories;
});
const {
data: products,
error: productsError,
loading: isProductLoading,
refresh: refreshProductRequest,
} = useRequest(async () => {
const response = await ProductApi.PublicGetProductList({
entity_type: ProductEntityType.ProjectTemplate,
page_num: 1,
page_size: MAX_PAGE_SIZE,
sort_type: SortType.Heat,
source: ProductListSource.Recommend,
is_free: true,
});
return response.data?.products;
});
const { copyProject, modalContextHolder } = useProjectTemplateCopyModal({
onSuccess: onCopyOk,
source: spaceId ? 'space' : 'navi',
onBefore: onBeforeCopy,
onError: onCopyError,
});
const refreshRequest = () => {
refreshCategoryRequest();
refreshProductRequest();
};
const isRequestLoading = isCategoryLoading || isProductLoading;
const isRequestError = Boolean(categoriesError || productsError);
if (!categories || !products) {
return (
<>
{modalContextHolder}
<div className="px-24px flex flex-col gap-y-[20px]">
<ProjectTemplateGroup title="基础">
<CreateEmptyProjectUI onClick={onCreateProject} />
{isRequestLoading ? (
<>
<CardSkeleton />
<CardSkeleton />
</>
) : null}
</ProjectTemplateGroup>
{isRequestLoading ? (
<>
<TemplateGroupSkeleton />
<TemplateGroupSkeleton />
</>
) : null}
{!isRequestLoading && isRequestError ? (
<Empty
className={styles['error-empty']}
image={<IllustrationFailure className="h-160px w-160px" />}
darkModeImage={
<IllustrationFailureDark className="h-160px w-160px" />
}
title={
<span className="coz-fg-primary text-[14px] font-medium leading-20px">
{I18n.t('creat_project_templates_load_failed')}
</span>
}
>
<Button onClick={refreshRequest} icon={<IconCozRefresh />}>
{I18n.t('Retry')}
</Button>
</Empty>
) : null}
</div>
</>
);
}
const baseTemplateList = products.filter(p =>
BASE_TEMPLATE_ID_LIST.some(id => id === p.meta_info.id),
);
const recommendTemplateList = xorBy(
products,
baseTemplateList,
p => p.meta_info.id,
);
const productGroupList = groupBy(
recommendTemplateList,
p => p.meta_info.category?.id,
);
const renderTemplateList = (productList: ProductInfo[]) =>
productList.map(product => (
<ProjectTemplateCard
viewSource={spaceId ? 'space' : 'navi'}
onClick={() => {
openTemplatePreview(product.meta_info.id ?? '');
}}
onCopyTemplate={param => {
copyProject({
spaceId,
isSelectSpace: isSelectSpaceOnCopy,
productId: param.id,
name: param.name,
sourceProduct: product,
});
}}
key={product.meta_info.id}
product={product}
/>
));
return (
<>
{modalContextHolder}
<div className="px-24px flex flex-col gap-y-[20px]">
<ProjectTemplateGroup title="基础">
<CreateEmptyProjectUI onClick={onCreateProject} />
{renderTemplateList(baseTemplateList)}
</ProjectTemplateGroup>
{categories.map(category => {
const productList = productGroupList[category.id ?? ''];
if (!productList?.length) {
return null;
}
return (
<ProjectTemplateGroup key={category.id} title={category.name}>
{renderTemplateList(productGroupList[category.id ?? ''] ?? [])}
</ProjectTemplateGroup>
);
})}
</div>
</>
);
};
export const ProjectTemplateModal: React.FC<
Omit<ModalProps, 'size' | 'title' | 'className' | 'footer'> &
ProjectTemplateBaseProps
> = ({
spaceId,
isSelectSpaceOnCopy,
onCopyOk,
onCreateProject,
onBeforeCopy,
onCopyError,
...props
}) => (
<Modal
size="xxl"
title={I18n.t('creat_project_templates')}
className={styles['project-template-modal']}
footer={null}
{...props}
>
<ProjectTemplateContent
spaceId={spaceId}
isSelectSpaceOnCopy={isSelectSpaceOnCopy}
onCopyOk={onCopyOk}
onCreateProject={onCreateProject}
onBeforeCopy={onBeforeCopy}
onCopyError={onCopyError}
/>
</Modal>
);

View File

@@ -0,0 +1,12 @@
.template-card-mask {
--template-card-mask-start: rgba(var(--coze-bg-3), 0);
--template-card-mask-end: rgba(var(--coze-bg-3), 0.96);
background: linear-gradient(180deg, var(--template-card-mask-start) 0%, var(--template-card-mask-end) 100%);
}
.template-group {
grid-template-columns: repeat(3, 1fr);
gap: 16px 1%;
// 根据 gpt 设置 1% 会根据父容器宽度计算
}

View File

@@ -0,0 +1,34 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { IconCozPlusFill } from '@coze-arch/coze-design/icons';
import { ProjectTemplateCardUI } from './project-template-card';
export const CreateEmptyProjectUI: React.FC<{
onClick: (() => void) | undefined;
}> = ({ onClick }) => (
<ProjectTemplateCardUI
onClick={onClick}
className="h-200px flex items-center justify-center flex-col coz-fg-primary"
>
<IconCozPlusFill />
<div className="py-6px px-8px text-[14px] leading-[20px] font-medium">
{I18n.t('creat_project_creat_new_project')}
</div>
</ProjectTemplateCardUI>
);

View File

@@ -0,0 +1,172 @@
/*
* 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 {
forwardRef,
type MouseEventHandler,
type PropsWithChildren,
useRef,
} from 'react';
import classNames from 'classnames';
import { useHover } from 'ahooks';
import { TeaExposure } from '@coze-studio/components';
import { type ProductInfo } from '@coze-arch/idl/product_api';
import { I18n } from '@coze-arch/i18n';
import { openNewWindow } from '@coze-arch/bot-utils';
import { extractTemplateActionCommonParams } from '@coze-arch/bot-tea/utils';
import {
EVENT_NAMES,
type ParamsTypeDefine,
sendTeaEvent,
} from '@coze-arch/bot-tea';
import { Button, Image } from '@coze-arch/coze-design';
import styles from './card.module.less';
export interface ProjectTemplateCardContentProps {
/** 埋点参数 页面来源 */
viewSource: ParamsTypeDefine[EVENT_NAMES.template_action_front]['source'];
product: ProductInfo;
onCopyTemplate?: (param: { name: string; id: string }) => void;
className?: string;
onClick?: () => void;
}
export const openTemplatePreview = (templateId: string) => {
const url = new URL(
`/template/project/${templateId}`,
window.location.origin,
);
openNewWindow(() => url.toString());
};
const ActionButton: React.FC<ProjectTemplateCardContentProps> = ({
viewSource,
product,
className,
onCopyTemplate,
}) => {
const onPreview: MouseEventHandler<HTMLButtonElement> = e => {
e.stopPropagation();
sendTeaEvent(EVENT_NAMES.template_action_front, {
action: 'click',
source: viewSource,
...extractTemplateActionCommonParams(product),
});
openTemplatePreview(product.meta_info.id ?? '');
};
const onCopy: MouseEventHandler<HTMLButtonElement> = e => {
e.stopPropagation();
onCopyTemplate?.({
name: product.meta_info.name ?? '',
id: product.meta_info.id ?? '',
});
};
const isShowCopyActionButton = !product.meta_info.is_professional;
return (
<div className={classNames('w-full px-12px', className)}>
<div
className={classNames('w-full h-24px', styles['template-card-mask'])}
/>
<div className="w-full flex justify-between pt-8px coz-bg-max gap-x-8px">
<Button color="highlight" className="flex-[1]" onClick={onPreview}>
{I18n.t('creat_project_use_template_preview')}
</Button>
{isShowCopyActionButton ? (
<Button color="hgltplus" className="flex-[1]" onClick={onCopy}>
{I18n.t('creat_project_use_template_use')}
</Button>
) : null}
</div>
</div>
);
};
export const ProjectTemplateCardUI = forwardRef<
HTMLDivElement,
PropsWithChildren<{ className?: string; onClick?: () => void }>
>(({ className, children, onClick }, ref) => (
<div
ref={ref}
onClick={onClick}
className={classNames(
'cursor-pointer p-12px coz-bg-max coz-stroke-primary border-solid border-[1px] hover:coz-shadow-default rounded-[16px]',
className,
)}
>
{children}
</div>
));
export const ProjectTemplateCard: React.FC<ProjectTemplateCardContentProps> = ({
viewSource,
product,
onCopyTemplate,
className,
onClick,
}) => {
const divRef = useRef<HTMLDivElement>(null);
const isHover = useHover(divRef);
return (
<ProjectTemplateCardUI
ref={divRef}
className={classNames('relative', className)}
onClick={() => {
sendTeaEvent(EVENT_NAMES.template_action_front, {
action: 'click',
source: viewSource,
...extractTemplateActionCommonParams(product),
});
onClick?.();
}}
>
<TeaExposure
once
teaEvent={{
name: EVENT_NAMES.template_action_front,
params: {
...extractTemplateActionCommonParams(product),
action: 'card_view',
source: viewSource,
},
}}
>
<div className="px-4px mb-8px overflow-hidden text-ellipsis coz-fg-primary text-[14px] font-medium leading-[20px]">
{product.meta_info.name}
</div>
<Image
preview={false}
src={product.meta_info.covers?.at(0)?.url}
className="rounded-[16px] block w-full"
imgCls="object-cover object-center w-full"
height={148}
/>
<ActionButton
viewSource={viewSource}
product={product}
onCopyTemplate={onCopyTemplate}
className={classNames(
'absolute left-0 bottom-[8px]',
!isHover && 'hidden',
)}
/>
</TeaExposure>
</ProjectTemplateCardUI>
);
};

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import styles from './card.module.less';
export interface ProjectTemplateGroupProps {
title: ReactNode | undefined;
groupChildrenClassName?: string;
}
export const ProjectTemplateGroup: React.FC<
PropsWithChildren<ProjectTemplateGroupProps>
> = ({ title, groupChildrenClassName, children }) => (
<div>
<div className="mb-8px coz-fg-plus text-[16px] font-medium leading-[22px]">
{title}
</div>
<div
className={classNames(
'grid',
styles['template-group'],
groupChildrenClassName,
)}
>
{children}
</div>
</div>
);

View File

@@ -0,0 +1,34 @@
/*
* 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 { Skeleton } from '@coze-arch/coze-design';
import { ProjectTemplateGroup } from './project-template-group';
export const CardSkeleton: React.FC = () => (
<Skeleton.Image className="rounded-xl" />
);
export const TemplateGroupSkeleton: React.FC = () => (
<ProjectTemplateGroup
title={<Skeleton.Title className="w-120px" />}
groupChildrenClassName="h-[200px]"
>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</ProjectTemplateGroup>
);

View File

@@ -0,0 +1,46 @@
/*
* 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 cls from 'classnames';
import { Switch, type SwitchProps } from '@coze-arch/coze-design';
export function SwitchWithDesc({
value,
onChange,
className,
desc,
descClassName,
switchClassName,
...rest
}: Omit<SwitchProps, 'checked'> & {
value?: boolean;
desc: string;
descClassName?: string;
switchClassName?: string;
}) {
return (
<div className={cls('flex items-center justify-between', className)}>
<span className={cls('coz-fg-primary', descClassName)}>{desc}</span>
<Switch
size="small"
{...rest}
checked={value}
onChange={onChange}
className={cls('shrink-0', switchClassName)}
/>
</div>
);
}

View File

@@ -0,0 +1,199 @@
/*
* 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 { useRequest } from 'ahooks';
import { appendCopySuffix } from '@coze-studio/components';
import {
type IntelligenceBasicInfo,
type User,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { intelligenceApi } from '@coze-arch/bot-api';
import { type RenderAutoGenerateParams } from '@coze-common/biz-components/picture-upload';
import { botInputLengthService } from '@coze-agent-ide/bot-input-length-limit';
import { commonProjectFormValid } from '../utils/common-project-form-valid';
import { ProjectFormModal } from '../components/project-form-modal';
import { type ProjectFormValues } from '../components/project-form';
export interface CopyProjectSuccessCallbackParam {
basicInfo: IntelligenceBasicInfo;
templateId: string;
ownerInfo?: User;
}
export interface UpdateProjectSuccessCallbackParam {
projectId: string;
spaceId: string;
}
type UseBaseUpdateOrCopyProjectModalProps = {
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
} & (
| {
scene: 'update';
onSuccess?: (params: UpdateProjectSuccessCallbackParam) => void;
}
| {
scene: 'copy';
onSuccess?: (param: CopyProjectSuccessCallbackParam) => void;
}
);
export const useBaseUpdateOrCopyProjectModal = ({
scene,
onSuccess: inputOnSuccess,
renderAutoGenerate,
}: UseBaseUpdateOrCopyProjectModalProps) => {
const [projectModalVisible, setProjectModalVisible] = useState(false);
const [initialValues, setInitialValues] = useState<ProjectFormValues>();
const onModalClose = () => {
setInitialValues(undefined);
setProjectModalVisible(false);
};
const onUpdateOk = (param: UpdateProjectSuccessCallbackParam) => {
onModalClose();
if (scene !== 'update') {
throw new Error('update project error scene');
}
inputOnSuccess?.(param);
};
const onCopyOK = (param: CopyProjectSuccessCallbackParam) => {
onModalClose();
if (scene !== 'copy') {
throw new Error('copy project error scene');
}
inputOnSuccess?.(param);
};
const onCancel = () => {
onModalClose();
};
const sharedProps = {
formProps: {
initValues: initialValues,
},
visible: projectModalVisible,
onCancel,
maskClosable: false,
};
const { runAsync: updateProjectRequest } = useRequest(
async (param: ProjectFormValues) => {
const { icon_uri: uriList, description = '', ...restValues } = param;
const requestFormValues = {
...restValues,
icon_uri: uriList?.at(0)?.uid,
description,
};
const response =
await intelligenceApi.DraftProjectUpdate(requestFormValues);
const { audit_data } = response.data ?? {};
return {
...audit_data,
};
},
{
manual: true,
onSuccess: (data, [inputParam]) => {
if (data.check_not_pass) {
return;
}
onUpdateOk({
projectId: inputParam.project_id,
spaceId: inputParam.space_id ?? '',
});
},
},
);
const { runAsync: copyProjectRequest } = useRequest(
async (param: ProjectFormValues) => {
const { icon_uri: uriList, ...restValues } = param;
const requestFormValues = {
...restValues,
icon_uri: uriList?.at(0)?.uid,
};
const response =
await intelligenceApi.DraftProjectCopy(requestFormValues);
const { audit_data, basic_info, user_info } = response.data ?? {};
return {
...audit_data,
basic_info,
user_info,
};
},
{
manual: true,
onSuccess: (data, [inputParam]) => {
if (!data.basic_info) {
return;
}
if (data.check_not_pass) {
return;
}
onCopyOK({
templateId: inputParam.project_id,
basicInfo: data.basic_info,
ownerInfo: data.user_info,
});
},
},
);
const getModalTitle = () => {
if (scene === 'copy') {
return I18n.t('project_ide_create_duplicate');
}
if (scene === 'update') {
return I18n.t('project_ide_edit_project');
}
};
return {
modalContextHolder: projectModalVisible ? (
<ProjectFormModal
{...sharedProps}
isFormValid={commonProjectFormValid}
title={getModalTitle()}
request={scene === 'update' ? updateProjectRequest : copyProjectRequest}
renderAutoGenerate={renderAutoGenerate}
/>
) : null,
openModal: ({ initialValue }: { initialValue: ProjectFormValues }) => {
setProjectModalVisible(true);
if (scene === 'update') {
setInitialValues(initialValue);
return;
}
if (scene === 'copy') {
setInitialValues({
...initialValue,
name: botInputLengthService.sliceStringByMaxLength({
value: appendCopySuffix(initialValue.name ?? ''),
field: 'projectName',
}),
});
}
},
};
};

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import { type DraftProjectCopyRequest } from '@coze-arch/idl/intelligence_api';
import { type RenderAutoGenerateParams } from '@coze-common/biz-components/picture-upload';
import {
type ModifyUploadValueType,
type RequireCopyProjectRequest,
} from '../type';
import {
type CopyProjectSuccessCallbackParam,
useBaseUpdateOrCopyProjectModal,
} from './use-base-update-or-copy-project-modal';
export const useCopyProjectModalBase = ({
onSuccess,
renderAutoGenerate,
}: {
onSuccess?: (param: CopyProjectSuccessCallbackParam) => void;
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
}): {
modalContextHolder: ReactNode;
openModal: (param: {
initialValue: ModifyUploadValueType<
RequireCopyProjectRequest<DraftProjectCopyRequest>
>;
}) => void;
} =>
useBaseUpdateOrCopyProjectModal({
scene: 'copy',
onSuccess,
renderAutoGenerate,
});

View File

@@ -0,0 +1,232 @@
/*
* 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 { useRequest } from 'ahooks';
import { useCreateAgent } from '@coze-studio/entity-adapter';
import { type RenderAutoGenerateParams } from '@coze-common/biz-components/picture-upload';
import { type DraftProjectCreateRequest } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { intelligenceApi } from '@coze-arch/bot-api';
import { commonProjectFormValid } from '../utils/common-project-form-valid';
import { ProjectTemplateModal } from '../components/project-template-modal';
import {
type BizProjectFormModalProps,
ProjectFormModal,
} from '../components/project-form-modal';
import { type ProjectFormValues } from '../components/project-form';
import {
type CreateType,
GuideModal,
type GuideModalProps,
} from '../components/guide-modal';
import {
type BeforeProjectTemplateCopyCallback,
type ProjectTemplateCopySuccessCallback,
} from './use-project-template-copy-modal';
type CreateBotParam = Parameters<typeof useCreateAgent>[0];
export interface CreateProjectSuccessCallbackParam {
projectId: string;
spaceId: string;
}
export interface CreateProjectHookProps
extends Pick<BizProjectFormModalProps, 'selectSpace'> {
onBeforeCreateBot?: CreateBotParam['onBefore'];
onCreateBotSuccess?: CreateBotParam['onSuccess'];
onCreateBotError?: CreateBotParam['onError'];
initialSpaceId?: string;
onBeforeCreateProject?: () => void;
onCreateProjectError?: () => void;
onCreateProjectSuccess?: (param: CreateProjectSuccessCallbackParam) => void;
onCopyProjectTemplateSuccess?: ProjectTemplateCopySuccessCallback;
onBeforeCopyProjectTemplate?: BeforeProjectTemplateCopyCallback;
onProjectTemplateCopyError?: () => void;
/**
* navi 导航栏
* space workspace 右上角的按钮
* */
bizCreateFrom: 'navi' | 'space';
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
extraGuideButtonConfigs?: GuideModalProps['extraButtonConfigs'];
}
// eslint-disable-next-line @coze-arch/max-line-per-function
export const useCreateProjectModalBase = ({
selectSpace,
onBeforeCreateBot,
onCreateBotError,
onCreateBotSuccess,
initialSpaceId,
onCreateProjectSuccess,
onCopyProjectTemplateSuccess,
onBeforeCreateProject,
onCreateProjectError,
onBeforeCopyProjectTemplate,
onProjectTemplateCopyError,
bizCreateFrom,
renderAutoGenerate,
extraGuideButtonConfigs,
}: CreateProjectHookProps) => {
const [guideModalVisible, setGuideModalVisible] = useState(false);
const [projectModalVisible, setProjectModalVisible] = useState(false);
const [projectTemplateModalVisible, setProjectTemplateModalVisible] =
useState(false);
const { modal, startEdit } = useCreateAgent({
showSpace: selectSpace,
onBefore: onBeforeCreateBot,
onError: onCreateBotError,
onSuccess: onCreateBotSuccess,
spaceId: initialSpaceId,
bizCreateFrom,
});
const onGuideChange = (guideType: CreateType) => {
setGuideModalVisible(false);
if (guideType === 'project') {
// 海外版和开源版不支持项目模板
if (IS_OVERSEA || IS_OPEN_SOURCE) {
setProjectModalVisible(true);
return;
}
setProjectTemplateModalVisible(true);
return;
}
if (guideType === 'agent') {
startEdit();
return;
}
};
const onCreateEmptyProject = () => {
setProjectModalVisible(true);
setProjectTemplateModalVisible(false);
};
const onGuideCancel = () => {
setGuideModalVisible(false);
};
const projectTemplateCancel = () => {
setProjectTemplateModalVisible(false);
};
const onCopyProjectTemplateOk: ProjectTemplateCopySuccessCallback =
params => {
setProjectTemplateModalVisible(false);
onCopyProjectTemplateSuccess?.(params);
};
const onCreateProjectOk = (param: CreateProjectSuccessCallbackParam) => {
setProjectModalVisible(false);
onCreateProjectSuccess?.(param);
};
const onCreateProjectCancel = () => {
setProjectModalVisible(false);
};
const { runAsync: createProjectRequest } = useRequest(
async (param: ProjectFormValues) => {
const { icon_uri: uriList, enableMonetize, ...restValues } = param;
const requestFormValues: DraftProjectCreateRequest = {
...restValues,
icon_uri: uriList?.at(0)?.uid,
...(IS_OVERSEA && {
monetization_conf: {
is_enable: enableMonetize ?? true,
},
}),
create_from: bizCreateFrom,
};
const response =
await intelligenceApi.DraftProjectCreate(requestFormValues);
const { project_id, audit_data } = response.data ?? {};
return {
...audit_data,
project_id: project_id ?? '',
};
},
{
manual: true,
onBefore: onBeforeCreateProject,
onError: onCreateProjectError,
onSuccess: (data, [inputParam]) => {
if (data.check_not_pass) {
return;
}
onCreateProjectOk({
projectId: data.project_id,
spaceId: inputParam.space_id ?? '',
});
},
},
);
return {
modalContextHolder: (
<>
{modal}
<ProjectTemplateModal
maskClosable={false}
onCreateProject={onCreateEmptyProject}
onBeforeCopy={onBeforeCopyProjectTemplate}
onCopyError={onProjectTemplateCopyError}
onCopyOk={onCopyProjectTemplateOk}
isSelectSpaceOnCopy={Boolean(selectSpace)}
spaceId={initialSpaceId}
visible={projectTemplateModalVisible}
onCancel={projectTemplateCancel}
/>
{guideModalVisible ? (
<GuideModal
visible={guideModalVisible}
onChange={onGuideChange}
onCancel={onGuideCancel}
extraButtonConfigs={extraGuideButtonConfigs}
/>
) : null}
{projectModalVisible ? (
<ProjectFormModal
showMonetizeConfig={IS_OVERSEA}
isFormValid={values =>
commonProjectFormValid(values) && Boolean(values.space_id)
}
maskClosable={false}
title={I18n.t('creat_project_title')}
formProps={{
initValues: {
space_id: initialSpaceId,
project_id: '',
},
}}
request={createProjectRequest}
selectSpace={selectSpace}
visible={projectModalVisible}
onCancel={onCreateProjectCancel}
renderAutoGenerate={renderAutoGenerate}
/>
) : null}
</>
),
createProject: () => {
setGuideModalVisible(true);
},
};
};

View File

@@ -0,0 +1,133 @@
/*
* 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 { useRef, useState } from 'react';
import { isObject } from 'lodash-es';
import { useRequest } from 'ahooks';
import { DeveloperApi, intelligenceApi } from '@coze-arch/bot-api';
import { DeleteProjectModal } from '../components/delete-project-modal';
interface DeleteAgentParam {
spaceId: string;
agentId: string;
}
interface DeleteProjectParam {
projectId: string;
}
const isAgentParam = (value: unknown): value is DeleteAgentParam =>
isObject(value) && 'agentId' in value;
const isProjectParam = (value: unknown): value is DeleteProjectParam =>
isObject(value) && 'projectId' in value;
export type DeleteIntelligenceParam = (
| DeleteAgentParam
| DeleteProjectParam
) & { name: string };
export const useDeleteIntelligence = (props?: {
onDeleteProjectSuccess?: (param: DeleteProjectParam) => void;
onDeleteAgentSuccess?: (param: DeleteAgentParam) => void;
}) => {
const [deleteIntelligenceName, setDeleteIntelligenceName] =
useState<string>('');
const deleteParamsRef = useRef<DeleteProjectParam | DeleteAgentParam>();
const [visible, setVisible] = useState<boolean>(false);
const [name, setName] = useState<string>();
const onCloseModal = () => {
setVisible(false);
setDeleteIntelligenceName('');
setName('');
deleteParamsRef.current = undefined;
};
const onDeleteCancel = () => {
onCloseModal();
};
const { loading, runAsync } = useRequest(
async (request: DeleteAgentParam | DeleteProjectParam) => {
if (isAgentParam(request)) {
const { spaceId, agentId } = request;
await DeveloperApi.DeleteDraftBot({
space_id: spaceId,
bot_id: agentId,
});
return;
}
if (isProjectParam(request)) {
const { projectId } = request;
await intelligenceApi.DraftProjectDelete({ project_id: projectId });
}
},
{
manual: true,
onSuccess: (_m, [p]) => {
onCloseModal();
if (isAgentParam(p)) {
props?.onDeleteAgentSuccess?.(p);
return;
}
if (isProjectParam(p)) {
props?.onDeleteProjectSuccess?.(p);
return;
}
},
},
);
const onDelete = () => {
if (!deleteParamsRef.current) {
return;
}
return runAsync(deleteParamsRef.current);
};
return {
modalContextHolder: (
<DeleteProjectModal
maskClosable={false}
value={name}
onChange={setName}
placeholder={deleteIntelligenceName}
visible={visible}
onCancel={onDeleteCancel}
onOk={onDelete}
okButtonProps={{
disabled: deleteIntelligenceName !== name,
loading,
}}
/>
),
deleteIntelligence: ({
name: deleteName,
...restParam
}: DeleteIntelligenceParam) => {
setVisible(true);
setDeleteIntelligenceName(deleteName);
deleteParamsRef.current = restParam;
},
};
};

View File

@@ -0,0 +1,44 @@
/*
* 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 { localStorageService } from '@coze-foundation/local-storage';
const SESSION_HIDDEN_KEY = 'coze-project-entity-hidden-key';
export const useHiddenSession = (key: string) => {
const [isSessionHidden, setIsSessionHidden] = useState(isKeyExist(key));
return {
isSessionHidden,
hideSession: () => {
if (isKeyExist(key)) {
return;
}
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY) || '';
localStorageService.setValue(
SESSION_HIDDEN_KEY,
oldValue ? `${oldValue},${key}` : key,
);
setIsSessionHidden(true);
},
};
};
const isKeyExist = (key: string) => {
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY);
return oldValue?.includes(key);
};

View File

@@ -0,0 +1,55 @@
/*
* 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';
export const useFormSubmitState = <T>({
initialValues,
getIsFormValid,
}: {
initialValues?: T;
getIsFormValid: (values: T) => boolean;
}) => {
const [isFormValid, setFormValid] = useState(
initialValues ? getIsFormValid(initialValues) : true,
);
const [isUploading, setUploading] = useState(false);
const checkFormValid = (values: T) => {
setFormValid(getIsFormValid(values));
};
const onValuesChange = (values: T) => {
checkFormValid(values);
};
const onBeforeUpload = () => {
setUploading(true);
};
const onAfterUpload = () => {
setUploading(false);
};
return {
isSubmitDisabled: !isFormValid || isUploading,
checkFormValid,
bizCallback: {
onValuesChange,
onBeforeUpload,
onAfterUpload,
},
};
};

View File

@@ -0,0 +1,181 @@
/*
* 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 { useRef, useState } from 'react';
import { useRequest } from 'ahooks';
import {
appendCopySuffix,
ProjectTemplateCopyModal,
type ProjectTemplateCopyValue,
} from '@coze-studio/components';
import { ProductEntityType } from '@coze-arch/idl/product_api';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
type ParamsTypeDefine,
sendTeaEvent,
} from '@coze-arch/bot-tea';
import { type ProductInfo } from '@coze-arch/bot-api/product_api';
import { ProductApi } from '@coze-arch/bot-api';
import { botInputLengthService } from '@coze-agent-ide/bot-input-length-limit';
import { type FormApi } from '@coze-arch/coze-design';
import { commonProjectFormValid } from '../utils/common-project-form-valid';
import { useFormSubmitState } from './use-project-form-submit-state';
export type ProjectTemplateCopySuccessCallback = (param: {
originProductId: string;
newEntityId: string;
toSpaceId: string;
}) => void;
export type BeforeProjectTemplateCopyCallback = (params: {
toSpaceId: string;
}) => void;
export const useProjectTemplateCopyModal = (props: {
onBefore?: BeforeProjectTemplateCopyCallback;
onError?: () => void;
onSuccess?: ProjectTemplateCopySuccessCallback;
/** 埋点参数 - 当前页面/来源 */
source: NonNullable<
ParamsTypeDefine[EVENT_NAMES.template_action_front]['source']
>;
}) => {
const [isSelectSpace, setSelectSpace] = useState(false);
const [visible, setVisible] = useState(false);
const [initValues, setInitValues] = useState<ProjectTemplateCopyValue>();
const [sourceProduct, setSourceProduct] = useState<ProductInfo>();
const formApi = useRef<FormApi<ProjectTemplateCopyValue>>();
const {
bizCallback: { onValuesChange },
isSubmitDisabled,
checkFormValid,
} = useFormSubmitState<ProjectTemplateCopyValue>({
getIsFormValid: values =>
commonProjectFormValid(values) && Boolean(values.spaceId),
});
const onModalClose = () => {
setVisible(false);
setInitValues(undefined);
formApi.current = undefined;
setSelectSpace(false);
};
const { run, loading } = useRequest(
async (copyRequestParam: ProjectTemplateCopyValue) => {
const { productId, spaceId, name } = copyRequestParam;
return ProductApi.PublicDuplicateProduct({
product_id: productId,
space_id: spaceId,
name,
entity_type: ProductEntityType.ProjectTemplate,
});
},
{
manual: true,
onBefore: ([inputParam]) => {
props.onBefore?.({ toSpaceId: inputParam.spaceId ?? '' });
},
onError: props.onError,
onSuccess: (data, [inputParam]) => {
onModalClose();
sendTeaEvent(EVENT_NAMES.template_action_front, {
template_id: sourceProduct?.meta_info.id || '',
template_name: sourceProduct?.meta_info?.name || '',
template_type: 'project',
entity_id: sourceProduct?.meta_info.entity_id || '',
entity_copy_id:
sourceProduct?.project_extra?.template_project_id || '',
template_tag_professional: sourceProduct?.meta_info.is_professional
? 'professional'
: 'basic',
action: 'duplicate',
after_id: data.data?.new_entity_id,
source: props.source,
...(sourceProduct?.meta_info?.is_free
? ({
template_tag_prize: 'free',
} as const)
: ({
template_tag_prize: 'paid',
template_prize_detail:
Number(sourceProduct?.meta_info?.price?.amount) || 0,
} as const)),
});
props?.onSuccess?.({
originProductId: inputParam?.productId ?? '',
newEntityId: data.data?.new_entity_id ?? '',
toSpaceId: inputParam?.spaceId ?? '',
});
},
},
);
return {
modalContextHolder: (
<ProjectTemplateCopyModal
title={I18n.t('creat_project_use_template')}
isSelectSpace={isSelectSpace}
visible={visible}
okButtonProps={{
disabled: isSubmitDisabled,
loading,
}}
maskClosable={false}
onOk={() => {
const requestValues = formApi.current?.getValues();
if (!requestValues) {
throw new Error('duplicate project template values not provided');
}
run(requestValues);
}}
onCancel={onModalClose}
formProps={{
initValues,
onValueChange: onValuesChange,
getFormApi: api => {
formApi.current = api;
},
}}
/>
),
copyProject: ({
isSelectSpace: inputIsSelectSpace,
sourceProduct: inputSourceProduct,
...rest
}: ProjectTemplateCopyValue & {
isSelectSpace: boolean;
/** 用于提取埋点参数 */
sourceProduct: ProductInfo;
}) => {
setSelectSpace(inputIsSelectSpace);
const fixedInitValues = {
...rest,
name: botInputLengthService.sliceStringByMaxLength({
value: appendCopySuffix(rest.name),
field: 'projectName',
}),
};
setInitValues(fixedInitValues);
checkFormValid(fixedInitValues);
setSourceProduct(inputSourceProduct);
setVisible(true);
},
};
};

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import { type DraftProjectUpdateRequest } from '@coze-arch/idl/intelligence_api';
import { type RenderAutoGenerateParams } from '@coze-common/biz-components/picture-upload';
import { type ModifyUploadValueType } from '../type';
import {
type UpdateProjectSuccessCallbackParam,
useBaseUpdateOrCopyProjectModal,
} from './use-base-update-or-copy-project-modal';
export const useUpdateProjectModalBase = ({
onSuccess,
renderAutoGenerate,
}: {
onSuccess?: (param: UpdateProjectSuccessCallbackParam) => void;
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
}): {
modalContextHolder: ReactNode;
openModal: (param: {
initialValue: ModifyUploadValueType<DraftProjectUpdateRequest>;
}) => void;
} =>
useBaseUpdateOrCopyProjectModal({
scene: 'update',
onSuccess,
renderAutoGenerate,
});

View File

@@ -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.
*/
export {
useCreateProjectModalBase,
type CreateProjectHookProps,
} from './hooks/use-create-project-modal';
export { useUpdateProjectModalBase } from './hooks/use-update-project-modal';
export {
useDeleteIntelligence,
type DeleteIntelligenceParam,
} from './hooks/use-delete-intelligence';
export { useCopyProjectModalBase } from './hooks/use-copy-project-modal';
export { type ProjectFormValues } from './components/project-form';
export {
type UpdateProjectSuccessCallbackParam,
type CopyProjectSuccessCallbackParam,
} from './hooks/use-base-update-or-copy-project-modal';
export {
type ModifyUploadValueType,
type RequireCopyProjectRequest,
} from './type';

View File

@@ -0,0 +1,29 @@
/*
* 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 UploadValue } from '@coze-common/biz-components';
export type ModifyUploadValueType<T extends { icon_uri?: string }> = Omit<
T,
'icon_uri'
> & { icon_uri?: UploadValue };
export type RequireCopyProjectRequest<
T extends { project_id?: string; to_space_id?: string },
> = Omit<T, 'project_id' | 'to_space_id'> & {
project_id: string;
to_space_id: string;
};

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' />

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.
*/
import { type ProjectFormValues } from '../components/project-form';
export const commonProjectFormValid = (
values: Pick<ProjectFormValues, 'name'>,
) => Boolean(values.name?.trim());