feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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% 会根据父容器宽度计算
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user