feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
.card-button {
cursor: pointer;
height: 32px;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-align: center;
border: 1px solid;
border-radius: 8px;
@apply coz-stroke-primary;
@apply coz-bg-primary;
@apply coz-fg-primary;
}

View File

@@ -0,0 +1,36 @@
/*
* 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 FC, type PropsWithChildren } from 'react';
import cls from 'classnames';
import styles from './index.module.less';
export const CardButton: FC<
PropsWithChildren<{
className?: string;
onClick?: () => void;
}>
> = ({ className, onClick, children }) => (
<button
className={cls(styles['card-button'], className)}
color="primary"
onClick={onClick}
>
{children}
</button>
);

View File

@@ -0,0 +1,42 @@
.container {
cursor: pointer;
position: relative;
margin-bottom: 20px;
padding: 16px;
border: 1px solid;
border-radius: 8px;
@apply coz-stroke-primary;
&:not(.skeleton):hover {
@apply coz-bg-max;
@apply coz-stroke-primary;
box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 6%);
&.shadow-primary {
box-shadow: 0 6px 8px 0 rgba(0, 8, 16, 12%);
}
}
&.skeleton {
cursor: default;
border-color: transparent;
}
.check {
position: absolute;
z-index: 1;
top: 16px;
right: 16px;
}
}
.width100 {
overflow: hidden;
width: 100%;
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import s from './index.module.less';
const Container = (props: {
className?: string;
children?: React.ReactNode;
shadowMode?: 'default' | 'primary';
onClick?: () => void;
}) => {
const { className, children, onClick, shadowMode } = props;
return (
<div
className={classNames(
'coz-bg-max',
s.container,
s.width100,
className,
s[`shadow-${shadowMode}`],
)}
onClick={onClick}
>
{children}
</div>
);
};
const SkeletonContainer = (props: {
children?: React.ReactNode;
className?: string;
}) => (
<div
className={classNames(
'coz-mg-primary',
s.container,
s.width100,
s.skeleton,
props.className,
)}
>
{props?.children}
</div>
);
export const CardContainer = Container;
export const CardSkeletonContainer = SkeletonContainer;

View File

@@ -0,0 +1,80 @@
/*
* 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 FC } from 'react';
import cls from 'classnames';
import { AvatarName } from '@coze-studio/components';
import { type explore } from '@coze-studio/api-schema';
import { type UserInfo as ProductUserInfo } from '@coze-arch/bot-api/product_api';
import { Typography } from '@coze-arch/coze-design';
type UserInfo = explore.product_common.UserInfo | ProductUserInfo;
interface TemplateCardBodyProps {
title?: string;
description?: string;
userInfo?: UserInfo;
descClassName?: string;
renderCardTag?: () => React.ReactNode;
renderDescBottomSlot?: () => React.ReactNode;
}
export const CardInfo: FC<TemplateCardBodyProps> = ({
title,
description,
userInfo,
renderCardTag,
descClassName,
renderDescBottomSlot,
}) => (
<div className={cls('mt-[8px] px-[4px] grow', 'flex flex-col')}>
<div className="flex items-center gap-[8px] overflow-hidden">
<Typography.Text
className="!font-medium text-[16px] leading-[22px] coz-fg-primary !max-w-[180px]"
ellipsis={{ showTooltip: true, rows: 1 }}
>
{title}
</Typography.Text>
{renderCardTag?.()}
</div>
<AvatarName
className="mt-[4px]"
avatar={userInfo?.avatar_url}
name={userInfo?.name}
username={userInfo?.user_name}
label={{
name: userInfo?.user_label?.label_name,
icon: userInfo?.user_label?.icon_url,
href: userInfo?.user_label?.jump_link,
}}
/>
<div
className={cls(
'mt-[8px] flex flex-col justify-between grow',
descClassName,
)}
>
<Typography.Text
className="min-h-[40px] leading-[20px] coz-fg-secondary"
ellipsis={{ showTooltip: true, rows: 2 }}
>
{description}
</Typography.Text>
{renderDescBottomSlot?.()}
</div>
</div>
);

View File

@@ -0,0 +1,78 @@
/*
* 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 { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
IconCozBot,
IconCozWorkflow,
IconCozWorkspace,
} from '@coze-arch/coze-design/icons';
import { Tag, type TagProps } from '@coze-arch/coze-design';
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
interface IProps {
type: ProductEntityType;
}
interface TagConfig {
icon: ReactNode;
i18nKey: I18nKeysNoOptionsType;
}
const TYPE_ICON_MAP: Partial<Record<ProductEntityType, TagConfig>> = {
[ProductEntityType.BotTemplate]: {
icon: <IconCozBot />,
i18nKey: 'template_agent',
},
[ProductEntityType.WorkflowTemplateV2]: {
icon: <IconCozWorkflow />,
i18nKey: 'template_workflow',
},
[ProductEntityType.ImageflowTemplateV2]: {
icon: <IconCozWorkflow />,
i18nKey: 'template_workflow',
},
[ProductEntityType.ProjectTemplate]: {
icon: <IconCozWorkspace />,
i18nKey: 'project_store_search',
},
};
const TYPE_COLOR_MAP: Partial<Record<ProductEntityType, TagProps['color']>> = {
[ProductEntityType.BotTemplate]: 'primary',
[ProductEntityType.WorkflowTemplateV2]: 'primary',
[ProductEntityType.ImageflowTemplateV2]: 'primary',
[ProductEntityType.ProjectTemplate]: 'brand',
};
export const CardTag = ({ type }: IProps) => {
const config = TYPE_ICON_MAP[type];
if (!config) {
return null;
}
return (
<Tag
color={TYPE_COLOR_MAP[type] ?? 'primary'}
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
>
{config.icon}
<span className="ml-[2px]">{I18n.t(config.i18nKey)}</span>
</Tag>
);
};

View File

@@ -0,0 +1,79 @@
.plugin {
margin-bottom: 0;
.plugin-wrapper {
position: relative;
}
.btn-container {
position: absolute;
bottom: 0;
left: 0;
display: none;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
&.one-column-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
&:hover {
.btn-container {
display: grid;
}
.description {
visibility: hidden;
}
}
.card-avatar {
width: 48px;
height: 48px;
border-radius: 6px;
/*
* 如此写边框和设置背景色的原因
* 1、边框是内边框处于图片的边缘上因此需要用after来写
* 2、背景色用before实体来写是由于 border和背景色都是透明色重叠会导致颜色加重出现边框
*/
&::after {
content: '';
position: absolute;
z-index: 2;
width: calc(100% - 2px);
height: calc(100% - 2px);
border-style: solid;
border-width: 1px;
border-radius: 6px;
@apply coz-stroke-primary;
}
&::before {
content: '';
position: absolute;
z-index: 1;
top: 1px;
left: 1px;
width: calc(100% - 2px);
height: calc(100% - 2px);
@apply bg-stroke-5;
border-radius: 5px;
}
& > img {
z-index: 2;
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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 FC } from 'react';
import cls from 'classnames';
import { type explore } from '@coze-studio/api-schema';
import { I18n } from '@coze-arch/i18n';
import { Avatar, Space, Tag, Toast, Tooltip } from '@coze-arch/coze-design';
import { cozeBaseUrl } from '@/const/url';
import { PluginAuthMode, type AuthMode } from '../type';
import { CardInfo } from '../components/info';
import { CardContainer, CardSkeletonContainer } from '../components/container';
import { CardButton } from '../components/button';
import styles from './index.module.less';
interface ProductInfo extends explore.ProductInfo {
plugin_extra: explore.ProductInfo['plugin_extra'] & AuthMode;
}
export type PluginCardProps = ProductInfo & {
isInstalled?: boolean;
isShowInstallButton?: boolean;
};
export const PluginCard: FC<PluginCardProps> = props => (
<CardContainer
className={styles.plugin}
shadowMode="default"
onClick={() => {
console.log('CardContainer...');
}}
>
<div className={styles['plugin-wrapper']}>
<PluginCardBody {...props} />
<Space
className={cls(styles['btn-container'], {
[styles['one-column-grid']]:
props.isInstalled || !props.isShowInstallButton,
})}
>
{!props.isInstalled && props.isShowInstallButton ? (
<CardButton
onClick={() => {
Toast.success(I18n.t('plugin_install_success'));
}}
>
{I18n.t('plugin_store_install')}
</CardButton>
) : null}
<CardButton
onClick={() => {
window.open(
`${cozeBaseUrl}/store/plugin/${props.meta_info?.id}?from=plugin_card`,
);
}}
>
{I18n.t('plugin_usage_limits_modal_view_details')}
</CardButton>
</Space>
</div>
</CardContainer>
);
export const PluginCardSkeleton = () => (
<CardSkeletonContainer className={cls('h-[186px]', styles.plugin)} />
);
const PluginCardBody: FC<PluginCardProps> = props => {
const renderCardTag = () => {
if (
props.plugin_extra.auth_mode === PluginAuthMode.Required ||
props.plugin_extra.auth_mode === PluginAuthMode.Supported
) {
return (
<Tag
color={'yellow'}
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
>
<span className="ml-[2px]">
{I18n.t('plugin_store_unauthorized')}
</span>
</Tag>
);
} else if (props.plugin_extra.auth_mode === PluginAuthMode.Configured) {
return (
<Tooltip content={I18n.t('plugin_store_contact_deployer')}>
<Tag
color={'brand'}
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
>
<span className="ml-[2px]">
{I18n.t('plugin_store_authorized')}
</span>
</Tag>
</Tooltip>
);
}
return null;
};
return (
<div>
<Avatar
className={styles['card-avatar']}
src={props.meta_info?.icon_url}
shape="square"
/>
<CardInfo
{...{
title: props.meta_info?.name,
description: props.meta_info?.description,
userInfo: props.meta_info?.user_info,
authMode: props.plugin_extra.auth_mode,
renderCardTag,
descClassName: styles.description,
}}
/>
</div>
);
};

View File

@@ -0,0 +1,28 @@
.template {
margin-bottom: 0;
.template-wrapper {
position: relative;
}
.btn-container {
position: absolute;
bottom: 0;
left: 0;
display: none;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
}
&:hover {
.btn-container {
display: grid;
}
.description {
visibility: hidden;
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* 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 FC, useState } from 'react';
import cls from 'classnames';
import { explore } from '@coze-studio/api-schema';
import { useSpaceList } from '@coze-foundation/space-store';
import { I18n } from '@coze-arch/i18n';
import { Image, Input, Modal, Space, Toast } from '@coze-arch/coze-design';
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
import { cozeBaseUrl } from '@/const/url';
import { type CardInfoProps } from '../type';
import { CardTag } from '../components/tag';
import { CardInfo } from '../components/info';
import { CardContainer, CardSkeletonContainer } from '../components/container';
import { CardButton } from '../components/button';
type ProductInfo = explore.ProductInfo;
import styles from './index.module.less';
export type TemplateCardProps = ProductInfo;
const PATH_MAP: Partial<
Record<explore.product_common.ProductEntityType, string>
> = {
[ProductEntityType.BotTemplate]: 'agent',
[ProductEntityType.WorkflowTemplateV2]: 'workflow',
[ProductEntityType.ImageflowTemplateV2]: 'workflow',
[ProductEntityType.ProjectTemplate]: 'project',
};
export const TemplateCard: FC<TemplateCardProps> = props => {
const [visible, setVisible] = useState(false);
return (
<CardContainer
className={styles.template}
shadowMode="default"
onClick={() => {
console.log('Template Click Card');
}}
>
<div className={styles['template-wrapper']}>
<TempCardBody
{...{
title: props.meta_info?.name,
description: props.meta_info?.description,
userInfo: props.meta_info?.user_info,
entityType: props.meta_info.entity_type,
imgSrc: props.meta_info.covers?.[0].url,
}}
/>
<Space className={styles['btn-container']}>
<CardButton
onClick={() => {
setVisible(true);
}}
>
{I18n.t('copy')}
</CardButton>
<CardButton
onClick={() => {
const pathPrefix = PATH_MAP[props.meta_info.entity_type] || '';
const pathSuffix = [
ProductEntityType.WorkflowTemplateV2,
ProductEntityType.ImageflowTemplateV2,
].includes(props.meta_info.entity_type)
? `?entity_type=${props.meta_info.entity_type}`
: '';
window.open(
`${cozeBaseUrl}/template/${pathPrefix}/${props.meta_info.id}${pathSuffix}`,
);
}}
>
{I18n.t('plugin_usage_limits_modal_view_details')}
</CardButton>
</Space>
</div>
{visible ? (
<DuplicateModal
productId={props.meta_info.id}
entityType={props.meta_info.entity_type}
defaultTitle={`${props.meta_info?.name}(${I18n.t('duplicate_rename_copy')})`}
hide={() => setVisible(false)}
/>
) : null}
</CardContainer>
);
};
const DuplicateModal: FC<{
defaultTitle: string;
productId: string;
entityType: explore.product_common.ProductEntityType;
hide: () => void;
}> = ({ defaultTitle, hide, productId, entityType }) => {
const [title, setTitle] = useState(defaultTitle);
const { spaces } = useSpaceList();
const spaceId = spaces?.[0]?.id;
console.log('title', title, spaces);
return (
<Modal
type="modal"
title={I18n.t('creat_project_use_template')}
visible={true}
onOk={async () => {
try {
await explore.PublicDuplicateProduct({
product_id: productId,
entity_type: entityType,
space_id: spaceId,
name: title,
});
Toast.success(I18n.t('copy_success'));
hide();
} catch (err) {
console.error('PublicDuplicateProduct', err);
Toast.error(I18n.t('copy_failed'));
}
}}
onCancel={hide}
cancelText={I18n.t('Cancel')}
okText={I18n.t('Confirm')}
>
<Space vertical spacing={4} className="w-full">
<Space className="w-full">
<span className="coz-fg-primary font-medium leading-[20px]">
{I18n.t('creat_project_project_name')}
</span>
<span className="coz-fg-hglt-red">*</span>
</Space>
<Input
className="w-full"
placeholder=""
defaultValue={defaultTitle}
onChange={value => {
setTitle(value);
}}
/>
</Space>
</Modal>
);
};
export const TemplateCardSkeleton = () => (
<CardSkeletonContainer className={cls('h-[278px]', styles.template)} />
);
export const TempCardBody: FC<
CardInfoProps & {
entityType?: explore.product_common.ProductEntityType | ProductEntityType;
renderImageBottomSlot?: () => React.ReactNode;
renderDescBottomSlot?: () => React.ReactNode;
}
> = ({
title,
imgSrc,
description,
entityType,
userInfo,
renderImageBottomSlot,
renderDescBottomSlot,
}) => (
<div>
<div className="relative w-full h-[140px] rounded-[8px] overflow-hidden">
<Image
preview={false}
src={imgSrc}
className="w-full h-full"
imgCls="w-full h-full object-cover object-center"
/>
{renderImageBottomSlot?.()}
</div>
<CardInfo
{...{
title,
description,
userInfo,
renderCardTag: () =>
entityType ? <CardTag type={entityType} /> : null,
descClassName: styles.description,
renderDescBottomSlot,
}}
/>
</div>
);

View File

@@ -0,0 +1,43 @@
/*
* 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 explore } from '@coze-studio/api-schema';
import { type UserInfo as ProductUserInfo } from '@coze-arch/bot-api/product_api';
type UserInfo = explore.product_common.UserInfo;
export interface CardInfoProps {
title?: string;
imgSrc?: string;
description?: string;
userInfo?: UserInfo | ProductUserInfo;
}
/** for open coze */
export enum PluginAuthMode {
/** No authorization required */
NoAuth = 0,
/** Authorization is required, but not configured */
Required = 1,
/** Authorization is required and has been configured */
Configured = 2,
/** Authorization is required, but the configuration can be empty */
Supported = 3,
}
export interface AuthMode {
/** for open coze */
auth_mode?: PluginAuthMode;
}