feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.spin {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
:global {
|
||||
.semi-spin-wrapper {
|
||||
line-height: 16px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--light-usage-primary-color-primary, #4D53E8);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 CSSProperties } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Spin } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './auto-load-more.module.less';
|
||||
|
||||
interface LoadMoreProps {
|
||||
loadingMore?: boolean;
|
||||
noMore?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function AutoLoadMore({
|
||||
loadingMore,
|
||||
noMore,
|
||||
className,
|
||||
style,
|
||||
}: LoadMoreProps) {
|
||||
if (noMore || !loadingMore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls(s.container, className)} style={style}>
|
||||
<Spin spinning={true} wrapperClassName={s.spin} />
|
||||
<div className={s.text}>{I18n.t('loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
line-height: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--semi-color-fill-0);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 0;
|
||||
padding: 0 12px;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { IconCloseNoCycle } from '@coze-arch/bot-icons';
|
||||
|
||||
import s from './sidesheet-title.module.less';
|
||||
|
||||
interface SideSheetTitleProps {
|
||||
icon?: ReactNode;
|
||||
title?: ReactNode;
|
||||
action?: ReactNode;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function SideSheetTitle({
|
||||
icon = <IconCloseNoCycle />,
|
||||
title,
|
||||
action,
|
||||
onClose,
|
||||
}: SideSheetTitleProps) {
|
||||
return (
|
||||
<div className={s.container}>
|
||||
{icon ? (
|
||||
<div className={s.icon} onClick={onClose}>
|
||||
{icon}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={s.title}>{title}</div>
|
||||
<div className={s.action}>{action}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
|
||||
.stop {
|
||||
width: 40px;
|
||||
margin-right: 16px;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.generate {
|
||||
:global {
|
||||
.semi-button-content-right {
|
||||
background-image: linear-gradient(90deg, #4D53E8, #4DCCE8);
|
||||
background-clip: text;
|
||||
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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, type CSSProperties } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip, UIButton } from '@coze-arch/bot-semi';
|
||||
import { IconEffects, IconStopOutlined } from '@coze-arch/bot-icons';
|
||||
import { useFlags } from '@coze-arch/bot-flags';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type NodeFormSchema } from '../../types';
|
||||
import { useInnerStore } from '../../store';
|
||||
import { useTestsetManageStore } from '../../hooks';
|
||||
import { TestsetManageEventName } from '../../events';
|
||||
import { toNodeFormSchemas } from './utils';
|
||||
|
||||
import s from './auto-fill.module.less';
|
||||
|
||||
interface AutoFillButtonProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
onAutoFill?: (schemas: NodeFormSchema[]) => void;
|
||||
}
|
||||
|
||||
/** AI生成节点数据的按钮 */
|
||||
export function AutoFillButton({
|
||||
className,
|
||||
style,
|
||||
onAutoFill,
|
||||
}: AutoFillButtonProps) {
|
||||
const [FLAGS] = useFlags();
|
||||
const { generating, patch } = useInnerStore();
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const { bizComponentSubject, bizCtx, reportEvent } = useTestsetManageStore(
|
||||
store => store,
|
||||
);
|
||||
const onClick = async () => {
|
||||
// report event
|
||||
reportEvent?.(TestsetManageEventName.AIGC_PARAMS_CLICK, {
|
||||
path: 'testset',
|
||||
});
|
||||
|
||||
patch({ generating: true });
|
||||
try {
|
||||
abortRef.current = new AbortController();
|
||||
const { genCaseData } = await debuggerApi.AutoGenerateCaseData(
|
||||
{ bizComponentSubject, bizCtx, count: 1 },
|
||||
{ signal: abortRef.current.signal },
|
||||
);
|
||||
|
||||
if (!genCaseData?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fill form values
|
||||
const autoSchemas = toNodeFormSchemas(genCaseData[0].input);
|
||||
onAutoFill?.(autoSchemas);
|
||||
} finally {
|
||||
patch({ generating: false });
|
||||
}
|
||||
};
|
||||
|
||||
const onStop = () => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
|
||||
// 社区版暂不支持该功能
|
||||
if (!FLAGS['bot.devops.testset_auto_gen'] || !(IS_OVERSEA || IS_BOE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls(s.wrapper, className)} style={style}>
|
||||
<Tooltip content={I18n.t('workflow_testset_stopgen')}>
|
||||
<UIButton
|
||||
icon={<IconStopOutlined />}
|
||||
className={cls(s.stop, generating || s.hidden)}
|
||||
onClick={onStop}
|
||||
/>
|
||||
</Tooltip>
|
||||
<UIButton
|
||||
loading={generating}
|
||||
icon={<IconEffects />}
|
||||
className={s.generate}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{generating
|
||||
? I18n.t('workflow_testset_generating')
|
||||
: I18n.t('workflow_testset_aigenerate')}
|
||||
</UIButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
.wrapper {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
color: var(--semi-color-text-0);
|
||||
|
||||
&.required::after {
|
||||
content: "*";
|
||||
font-weight: 600;
|
||||
color: var(--semi-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.type-label {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
color: var(--light-usage-text-color-text-3, rgb(29 28 35 / 35%));
|
||||
}
|
||||
}
|
||||
@@ -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 CSSProperties } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
import s from './form-label.module.less';
|
||||
|
||||
interface FormLabelProps {
|
||||
label: string;
|
||||
typeLabel?: string;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
// 内置的FormLabel样式不支持 typeLabel,所以简单自定义
|
||||
export function FormLabel({
|
||||
label,
|
||||
typeLabel,
|
||||
required,
|
||||
className,
|
||||
style,
|
||||
}: FormLabelProps) {
|
||||
return (
|
||||
<div className={cls(s.wrapper, className)} style={style}>
|
||||
<div className={cls(s.label, required && s.required)}>{label}</div>
|
||||
{typeLabel ? <div className={s['type-label']}>{typeLabel}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill="none">
|
||||
<path d="M10.6777 4.83398L4.01387 11.9738C3.8246 12.1766 3.8246 12.4913 4.01387 12.6941L10.6777 19.834"
|
||||
stroke="#1D1C23" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M20 12.334H4" stroke="#1D1C23" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -0,0 +1,60 @@
|
||||
.sidesheet {
|
||||
:global {
|
||||
.semi-sidesheet-title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.semi-sidesheet-content {
|
||||
background-color: var(--light-color-grey-grey-0, #F7F7FA);
|
||||
}
|
||||
|
||||
.semi-form-field-label-text {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
&::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 不要红色感叹号 与workflow对齐
|
||||
.semi-form-field-error-message .semi-form-field-validate-status-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.testset-desc {
|
||||
:global {
|
||||
.semi-input-textarea-counter {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: var(--light-usage-text-color-text-3, rgb(29 28 35 / 35%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-data-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 12px 0 16px;
|
||||
|
||||
>span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 24px;
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
}
|
||||
|
||||
.auto-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type FormApi } from '@coze-arch/bot-semi/Form';
|
||||
import {
|
||||
UIButton,
|
||||
Form,
|
||||
UIFormTextArea,
|
||||
SideSheet,
|
||||
Spin,
|
||||
Tooltip,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { SideSheetTitle } from '../sidesheet-title';
|
||||
import {
|
||||
type FormItemSchema,
|
||||
type TestsetData,
|
||||
type TestsetDatabase,
|
||||
FormItemSchemaType,
|
||||
type NodeFormSchema,
|
||||
} from '../../types';
|
||||
import { useInnerStore } from '../../store';
|
||||
import { useTestsetManageStore } from '../../hooks';
|
||||
import { TestsetManageEventName } from '../../events';
|
||||
import {
|
||||
toNodeFormSchemas,
|
||||
isNil,
|
||||
traverseNodeFormSchemas,
|
||||
getTestsetNameRules,
|
||||
getSubFieldName,
|
||||
isSameType,
|
||||
transBoolSelect2Bool,
|
||||
type ValuesForBoolSelect,
|
||||
transFormItemSchema2Form,
|
||||
} from './utils';
|
||||
import { TestsetNameInput } from './testset-name-input';
|
||||
import { NodeFormSection } from './node-form-section';
|
||||
import { ReactComponent as IconBack } from './icon-back.svg';
|
||||
import { AutoFillButton } from './auto-fill';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface TestsetEditState {
|
||||
visible?: boolean;
|
||||
mode?: 'edit' | 'create';
|
||||
testset?: TestsetData;
|
||||
}
|
||||
|
||||
interface TestsetEditSideSheetProps extends TestsetEditState {
|
||||
mask?: boolean;
|
||||
onClose?: () => void;
|
||||
onSuccess?: (testset?: TestsetData) => void;
|
||||
onCancel?: () => void;
|
||||
/** 是否为多人协作模式 */
|
||||
isExpertMode?: boolean;
|
||||
}
|
||||
|
||||
const TESTSET_NAME_FIELD = '__TESTSET_NAME__';
|
||||
const TESTSET_DESC_FIELD = '__TESTSET_DESC__';
|
||||
|
||||
/**
|
||||
* 特化逻辑:表单项赋默认值
|
||||
* - Boolean类型:`false` 因为undefined的表现上和false一样,容易引发用户误解
|
||||
* - Object类型:`{}`
|
||||
* - Array类型: `[]`
|
||||
*/
|
||||
function assignDefaultValue(ipt: FormItemSchema) {
|
||||
if (!isNil(ipt.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (ipt.type) {
|
||||
case FormItemSchemaType.BOOLEAN:
|
||||
// ipt.value = true;
|
||||
break;
|
||||
case FormItemSchemaType.OBJECT:
|
||||
ipt.value = '{}';
|
||||
break;
|
||||
case FormItemSchemaType.LIST:
|
||||
ipt.value = '[]';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
interface TestsetFormValue {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function -- form function component
|
||||
export function TestsetEditSideSheet({
|
||||
visible,
|
||||
mode,
|
||||
testset,
|
||||
mask,
|
||||
onClose,
|
||||
onSuccess,
|
||||
isExpertMode,
|
||||
}: TestsetEditSideSheetProps) {
|
||||
const { generating: autoGenerating } = useInnerStore();
|
||||
const { bizComponentSubject, bizCtx, reportEvent } = useTestsetManageStore(
|
||||
store => store,
|
||||
);
|
||||
|
||||
const testsetFormApi = useRef<FormApi<TestsetFormValue>>();
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const { data: nodeSchemas, loading: loadingSchema } = useRequest(
|
||||
async () => {
|
||||
if (!visible) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const localSchemas = toNodeFormSchemas(testset?.caseBase?.input);
|
||||
const res = await debuggerApi.GetSchemaByID({
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
});
|
||||
const remoteSchemas = toNodeFormSchemas(res.schemaJson);
|
||||
|
||||
if (localSchemas.length) {
|
||||
// 编辑模式:比对本地和远程schema并尝试赋值
|
||||
const localSchemaMap: Record<string, FormItemSchema | undefined> = {};
|
||||
traverseNodeFormSchemas(
|
||||
localSchemas,
|
||||
(schema, ipt) => (localSchemaMap[getSubFieldName(schema, ipt)] = ipt),
|
||||
);
|
||||
|
||||
traverseNodeFormSchemas(remoteSchemas, (schema, ipt) => {
|
||||
const subName = getSubFieldName(schema, ipt);
|
||||
const field = localSchemaMap[subName];
|
||||
|
||||
if (isSameType(ipt.type, field?.type) && !isNil(field?.value)) {
|
||||
ipt.value = field?.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 创建模式:赋默认值
|
||||
traverseNodeFormSchemas(remoteSchemas, (schema, ipt) => {
|
||||
assignDefaultValue(ipt);
|
||||
});
|
||||
}
|
||||
|
||||
return remoteSchemas;
|
||||
},
|
||||
{ refreshDeps: [testset, visible] },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
testsetFormApi.current?.setValues({
|
||||
[TESTSET_NAME_FIELD]: testset?.caseBase?.name ?? '',
|
||||
[TESTSET_DESC_FIELD]: testset?.caseBase?.description,
|
||||
});
|
||||
}, [visible, testset]);
|
||||
|
||||
// 给节点表单设置值
|
||||
useEffect(() => {
|
||||
if (typeof nodeSchemas === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = testsetFormApi.current?.getValues() ?? {};
|
||||
traverseNodeFormSchemas(
|
||||
nodeSchemas,
|
||||
(schema, ipt) =>
|
||||
(values[getSubFieldName(schema, ipt)] =
|
||||
transFormItemSchema2Form(ipt)?.value),
|
||||
);
|
||||
testsetFormApi.current?.setValues(values);
|
||||
}, [nodeSchemas]);
|
||||
|
||||
const renderTitle = () => (
|
||||
<SideSheetTitle
|
||||
icon={<IconBack />}
|
||||
title={
|
||||
mode
|
||||
? mode === 'create'
|
||||
? I18n.t('workflow_testset_create_title')
|
||||
: I18n.t('workflow_testset_edit_title')
|
||||
: ''
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderFooter = () => (
|
||||
<div className="text-right">
|
||||
<Tooltip
|
||||
trigger={isExpertMode ? 'hover' : 'custom'}
|
||||
content={I18n.t('workflow_testset_submit_tooltip_for_expert_mode')}
|
||||
>
|
||||
<UIButton
|
||||
theme="solid"
|
||||
disabled={autoGenerating}
|
||||
loading={validating || submitting}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{I18n.t('workflow_testset_edit_confirm')}
|
||||
</UIButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderNodeForm = () => {
|
||||
if (loadingSchema) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
if (!nodeSchemas?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeSchemas.map(schema => (
|
||||
<NodeFormSection
|
||||
key={schema.component_id}
|
||||
schema={schema}
|
||||
autoGenerating={autoGenerating}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const onConfirm = async () => {
|
||||
setValidating(true);
|
||||
try {
|
||||
await testsetFormApi.current?.validate();
|
||||
const errors = testsetFormApi.current?.getFormState().errors;
|
||||
|
||||
if (Object.keys(errors ?? {}).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit();
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const onSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const testsetFormValues = testsetFormApi.current?.getValues();
|
||||
|
||||
if (!testsetFormValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputSchemas = cloneDeep(nodeSchemas ?? []);
|
||||
traverseNodeFormSchemas(inputSchemas, (schema, ipt) => {
|
||||
const val = testsetFormValues[getSubFieldName(schema, ipt)];
|
||||
|
||||
if (!isNil(val)) {
|
||||
ipt.value = val;
|
||||
}
|
||||
|
||||
// 清除 object/array的空值,包括空字符串
|
||||
if (
|
||||
!val &&
|
||||
(ipt.type === FormItemSchemaType.LIST ||
|
||||
ipt.type === FormItemSchemaType.OBJECT)
|
||||
) {
|
||||
ipt.value = undefined;
|
||||
}
|
||||
|
||||
// bool 类型 需要将枚举转为布尔值
|
||||
if (ipt.type === FormItemSchemaType.BOOLEAN) {
|
||||
ipt.value = transBoolSelect2Bool(ipt.value as ValuesForBoolSelect);
|
||||
}
|
||||
});
|
||||
|
||||
const caseBase: TestsetDatabase = {
|
||||
name: testsetFormValues[TESTSET_NAME_FIELD],
|
||||
caseID: testset?.caseBase?.caseID,
|
||||
description: testsetFormValues[TESTSET_DESC_FIELD],
|
||||
input: JSON.stringify(inputSchemas),
|
||||
};
|
||||
|
||||
const saveResp = await debuggerApi.SaveCaseData({
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
caseBase,
|
||||
});
|
||||
|
||||
if (mode === 'create') {
|
||||
reportEvent?.(TestsetManageEventName.CREATE_TESTSET_SUCCESS);
|
||||
}
|
||||
onSuccess?.(saveResp.caseDetail);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** auto fill form values */
|
||||
const onAutoFill = (autoSchemas: NodeFormSchema[]) => {
|
||||
const formValues = testsetFormApi.current?.getValues() || {};
|
||||
const validateFields: string[] = [];
|
||||
|
||||
traverseNodeFormSchemas(autoSchemas, (schema, ipt) => {
|
||||
const fieldName = getSubFieldName(schema, ipt);
|
||||
const value = transFormItemSchema2Form(ipt)?.value;
|
||||
if (!isNil(value)) {
|
||||
formValues[fieldName] = value;
|
||||
validateFields.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
testsetFormApi.current?.setValues(formValues);
|
||||
// 设置值之后再校验一次
|
||||
testsetFormApi.current?.validate(validateFields);
|
||||
};
|
||||
|
||||
return (
|
||||
<SideSheet
|
||||
title={renderTitle()}
|
||||
footer={renderFooter()}
|
||||
visible={visible}
|
||||
mask={mask}
|
||||
className={s.sidesheet}
|
||||
width={600}
|
||||
closable={false}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<Form<TestsetFormValue>
|
||||
getFormApi={api => (testsetFormApi.current = api)}
|
||||
>
|
||||
<TestsetNameInput
|
||||
field={TESTSET_NAME_FIELD}
|
||||
trigger="blur"
|
||||
stopValidateWithError={true}
|
||||
label={I18n.t('workflow_testset_name')}
|
||||
placeholder={I18n.t('workflow_testset_name_placeholder')}
|
||||
rules={getTestsetNameRules({
|
||||
bizCtx,
|
||||
bizComponentSubject,
|
||||
originVal: testset?.caseBase?.name,
|
||||
isOversea: IS_OVERSEA,
|
||||
})}
|
||||
/>
|
||||
<UIFormTextArea
|
||||
field={TESTSET_DESC_FIELD}
|
||||
className={s['testset-desc']}
|
||||
label={I18n.t('workflow_testset_desc')}
|
||||
placeholder={I18n.t('workflow_testset_desc_placeholder')}
|
||||
autosize={true}
|
||||
maxCount={200}
|
||||
maxLength={200}
|
||||
rows={2}
|
||||
/>
|
||||
<div className={s['node-data-title']}>
|
||||
<span>{I18n.t('workflow_testset_node_data')}</span>
|
||||
<AutoFillButton className={s['auto-btn']} onAutoFill={onAutoFill} />
|
||||
</div>
|
||||
{renderNodeForm()}
|
||||
</Form>
|
||||
</SideSheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
.section {
|
||||
:global .semi-form-section-text {
|
||||
border-bottom: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 22px;
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
}
|
||||
|
||||
.icon {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 8px;
|
||||
|
||||
font-size: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
|
||||
.select-container {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties, Fragment } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form, UIFormSelect, UIFormTextArea } from '@coze-arch/bot-semi';
|
||||
import { ComponentType } from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import {
|
||||
type NodeFormSchema,
|
||||
type FormItemSchema,
|
||||
FormItemSchemaType,
|
||||
} from '../../types';
|
||||
import { useTestsetManageStore } from '../../hooks';
|
||||
import {
|
||||
getCustomProps,
|
||||
getLabel,
|
||||
getPlaceholder,
|
||||
getSubFieldName,
|
||||
getTypeLabel,
|
||||
optionsForBoolSelect,
|
||||
} from './utils';
|
||||
import { FormLabel } from './form-label';
|
||||
|
||||
import s from './node-form-section.module.less';
|
||||
|
||||
interface NodeFormSectionProps {
|
||||
schema: NodeFormSchema;
|
||||
/** AI生成中 */
|
||||
autoGenerating?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const { Section, InputNumber } = Form;
|
||||
|
||||
/** 整数类型表单精度 */
|
||||
const INTEGER_PRECISION = 0.1;
|
||||
|
||||
export function NodeFormSection({
|
||||
schema,
|
||||
autoGenerating,
|
||||
className,
|
||||
style,
|
||||
}: NodeFormSectionProps) {
|
||||
const formRenders = useTestsetManageStore(store => store.formRenders);
|
||||
|
||||
const renderSectionTitle = () => {
|
||||
let sectionName = schema.component_name;
|
||||
// 目前只有start和variable两种节点
|
||||
switch (schema.component_type) {
|
||||
case ComponentType.CozeStartNode:
|
||||
sectionName = I18n.t('workflow_testset_start_node');
|
||||
break;
|
||||
case ComponentType.CozeVariableBot:
|
||||
sectionName = I18n.t('workflow_testset_vardatabase_node');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.title}>
|
||||
{schema.component_icon ? (
|
||||
<div className={s.icon}>
|
||||
<img src={schema.component_icon} />
|
||||
</div>
|
||||
) : null}
|
||||
{sectionName}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFormItem = (formItemSchema: FormItemSchema) => {
|
||||
const { type, name, required } = formItemSchema;
|
||||
const CustomFormItem = formRenders?.[type];
|
||||
const fieldName = getSubFieldName(schema, formItemSchema);
|
||||
const placeholder = getPlaceholder(formItemSchema);
|
||||
const requiredMsg = I18n.t('workflow_testset_required_tip', {
|
||||
param_name: formItemSchema.type === FormItemSchemaType.BOT ? '' : name,
|
||||
});
|
||||
|
||||
if (typeof CustomFormItem !== 'undefined') {
|
||||
return (
|
||||
<CustomFormItem
|
||||
field={fieldName}
|
||||
disabled={autoGenerating}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
{...getCustomProps(formItemSchema)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FormItemSchemaType.BOOLEAN:
|
||||
return (
|
||||
<UIFormSelect
|
||||
className={s['select-container']}
|
||||
field={fieldName}
|
||||
disabled={autoGenerating}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
optionList={optionsForBoolSelect}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
showClear={!required}
|
||||
/>
|
||||
);
|
||||
case FormItemSchemaType.INTEGER:
|
||||
case FormItemSchemaType.FLOAT:
|
||||
case FormItemSchemaType.NUMBER:
|
||||
return (
|
||||
<InputNumber
|
||||
field={fieldName}
|
||||
trigger={['change', 'blur']}
|
||||
precision={
|
||||
type === FormItemSchemaType.INTEGER
|
||||
? INTEGER_PRECISION
|
||||
: undefined
|
||||
}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
disabled={autoGenerating}
|
||||
noLabel={true}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
case FormItemSchemaType.OBJECT:
|
||||
case FormItemSchemaType.LIST:
|
||||
return (
|
||||
<UIFormTextArea
|
||||
field={fieldName}
|
||||
trigger={['change', 'blur']}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
disabled={autoGenerating}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
case FormItemSchemaType.STRING:
|
||||
default:
|
||||
return (
|
||||
<UIFormTextArea
|
||||
field={fieldName}
|
||||
autosize={{ minRows: 2, maxRows: 5 }}
|
||||
trigger={['change', 'blur']}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
disabled={autoGenerating}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Section
|
||||
className={cls(s.section, className)}
|
||||
style={style}
|
||||
text={renderSectionTitle()}
|
||||
>
|
||||
{schema.inputs.map((formItemSchema, i) => (
|
||||
<Fragment key={i}>
|
||||
<FormLabel
|
||||
className={s.label}
|
||||
label={getLabel(formItemSchema)}
|
||||
typeLabel={getTypeLabel(formItemSchema)}
|
||||
required={formItemSchema.required}
|
||||
/>
|
||||
{renderFormItem(formItemSchema)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.suffix {
|
||||
margin-right: 12px;
|
||||
margin-left: 8px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: var(--light-usage-text-color-text-3, rgb(29 28 35 / 35%));
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 ChangeEvent, type FocusEvent } from 'react';
|
||||
|
||||
import { type InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import { withField, UIInput } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './testset-name-input.module.less';
|
||||
|
||||
const TESTSET_NAME_MAX_LEN = 50;
|
||||
|
||||
function count(val: unknown) {
|
||||
return val ? `${val}`.length : 0;
|
||||
}
|
||||
|
||||
/** 需要后缀 & blur trim,扩展下原始的input */
|
||||
function InnerInput(props: InputProps) {
|
||||
const onBlur = (evt: FocusEvent<HTMLInputElement>) => {
|
||||
props.onChange?.(
|
||||
`${props.value ?? ''}`.trim(),
|
||||
{} as unknown as ChangeEvent<HTMLInputElement>,
|
||||
);
|
||||
props.onBlur?.(evt);
|
||||
};
|
||||
|
||||
return (
|
||||
<UIInput
|
||||
{...props}
|
||||
maxLength={props.maxLength ?? TESTSET_NAME_MAX_LEN}
|
||||
autoComplete="off"
|
||||
onBlur={onBlur}
|
||||
suffix={
|
||||
<div className={s.suffix}>
|
||||
{count(props.value)}/{props.maxLength ?? TESTSET_NAME_MAX_LEN}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const TestsetNameInput = withField(InnerInput, {}) as (
|
||||
props: CommonFieldProps & InputProps,
|
||||
) => JSX.Element;
|
||||
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- copy */
|
||||
import Ajv from 'ajv';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type RuleItem } from '@coze-arch/bot-semi/Form';
|
||||
import {
|
||||
type ComponentSubject,
|
||||
type BizCtx,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import {
|
||||
type NodeFormSchema,
|
||||
type FormItemSchema,
|
||||
type ArrayFieldSchema,
|
||||
FormItemSchemaType,
|
||||
} from '../../types';
|
||||
|
||||
let ajv: Ajv | undefined;
|
||||
/** jsonStr转为节点表单schema(简单的`JSON.parse`) */
|
||||
export function toNodeFormSchemas(jsonStr?: string): NodeFormSchema[] {
|
||||
if (!jsonStr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const schemas = JSON.parse(jsonStr) as NodeFormSchema[];
|
||||
return schemas;
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 空值判断,null/undefined/NaN */
|
||||
export function isNil(val: unknown) {
|
||||
return (
|
||||
typeof val === 'undefined' ||
|
||||
val === null ||
|
||||
(typeof val === 'number' && isNaN(val))
|
||||
);
|
||||
}
|
||||
|
||||
function isNumberType(t: string) {
|
||||
return t === FormItemSchemaType.NUMBER || t === FormItemSchemaType.FLOAT;
|
||||
}
|
||||
|
||||
/** 判断类型一致,**特化:**`number`和`float`视为同一类型 */
|
||||
export function isSameType(t1?: string, t2?: string) {
|
||||
if (typeof t1 === 'undefined' || typeof t2 === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isNumberType(t1) ? isNumberType(t2) : t1 === t2;
|
||||
}
|
||||
|
||||
/** 两层for遍历schema (经常需要遍历,单独抽一个的函数) */
|
||||
export function traverseNodeFormSchemas(
|
||||
schemas: NodeFormSchema[],
|
||||
cb: (s: NodeFormSchema, ip: FormItemSchema) => any,
|
||||
) {
|
||||
for (const schema of schemas) {
|
||||
for (const ipt of schema.inputs) {
|
||||
cb(schema, ipt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验名称格式(参考插件名称)
|
||||
* - 海外:仅支持输入字母、数字、下划线或空格
|
||||
* - 国内:仅支持输入中文、字母、数字、下划线或空格
|
||||
*/
|
||||
function validateNamePattern(
|
||||
name: string,
|
||||
isOversea?: boolean,
|
||||
): string | undefined {
|
||||
try {
|
||||
const pattern = isOversea ? /^[\w\s]+$/ : /^[\w\s\u4e00-\u9fa5]+$/u;
|
||||
const msg = isOversea
|
||||
? I18n.t('create_plugin_modal_nameerror')
|
||||
: I18n.t('create_plugin_modal_nameerror_cn');
|
||||
|
||||
return pattern.test(name) ? undefined : msg;
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface GetTestsetNameRulesProps {
|
||||
/** bizCtx */
|
||||
bizCtx?: BizCtx;
|
||||
/** bizComponentSubject */
|
||||
bizComponentSubject?: ComponentSubject;
|
||||
/** 原始值 */
|
||||
originVal?: string;
|
||||
/** 是否为海外(海外不允许输入中文 ,与PluginName校验规则对齐) */
|
||||
isOversea?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Testset名称表单校验规则
|
||||
*
|
||||
* @param param.bizCtx - bizCtx
|
||||
* @param param.bizComponentSubject - bizComponentSubject
|
||||
* @param param.originVal - 原始值
|
||||
* @param param.isOversea - 是否为海外(海外不允许输入中文 ,与PluginName校验规则对齐)
|
||||
*/
|
||||
export function getTestsetNameRules({
|
||||
bizCtx,
|
||||
bizComponentSubject,
|
||||
originVal,
|
||||
isOversea,
|
||||
}: GetTestsetNameRulesProps): RuleItem[] {
|
||||
const requiredMsg = I18n.t('workflow_testset_required_tip', {
|
||||
param_name: I18n.t('workflow_testset_name'),
|
||||
});
|
||||
|
||||
return [
|
||||
{ required: true, message: requiredMsg },
|
||||
{
|
||||
asyncValidator: async (_rules, value: string, cb) => {
|
||||
// required
|
||||
if (!value) {
|
||||
cb(requiredMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑模式下,名称与原名相同时跳过
|
||||
if (originVal && value === originVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 中文、字母等等等等
|
||||
const formatMsg = validateNamePattern(value, isOversea);
|
||||
|
||||
if (formatMsg) {
|
||||
cb(formatMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查重复
|
||||
try {
|
||||
const { isPass } = await debuggerApi.CheckCaseDuplicate({
|
||||
bizCtx,
|
||||
bizComponentSubject,
|
||||
caseName: value,
|
||||
});
|
||||
|
||||
if (isPass) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
cb(I18n.t('workflow_testset_name_duplicated'));
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch -- no catch
|
||||
} catch {
|
||||
cb();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单label
|
||||
* - bot:选择你需要的Bot
|
||||
* - 其他:字段名
|
||||
*/
|
||||
export function getLabel(formSchema: FormItemSchema) {
|
||||
return formSchema.type === FormItemSchemaType.BOT
|
||||
? I18n.t('workflow_testset_vardatabase_tip')
|
||||
: formSchema.name;
|
||||
}
|
||||
|
||||
function getSubType(type: string) {
|
||||
switch (type) {
|
||||
case FormItemSchemaType.STRING:
|
||||
return 'String';
|
||||
case FormItemSchemaType.FLOAT:
|
||||
case FormItemSchemaType.NUMBER:
|
||||
return 'Number';
|
||||
case FormItemSchemaType.OBJECT:
|
||||
return 'Object';
|
||||
case FormItemSchemaType.BOOLEAN:
|
||||
return 'Boolean';
|
||||
case FormItemSchemaType.INTEGER:
|
||||
return 'Integer';
|
||||
default:
|
||||
return `${type.charAt(0).toUpperCase()}${type.slice(1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** 类型标签 */
|
||||
export function getTypeLabel(formSchema: FormItemSchema) {
|
||||
switch (formSchema.type) {
|
||||
case FormItemSchemaType.STRING:
|
||||
case FormItemSchemaType.FLOAT:
|
||||
case FormItemSchemaType.NUMBER:
|
||||
case FormItemSchemaType.OBJECT:
|
||||
case FormItemSchemaType.BOOLEAN:
|
||||
case FormItemSchemaType.INTEGER:
|
||||
return getSubType(formSchema.type);
|
||||
case FormItemSchemaType.LIST: {
|
||||
const subType = (formSchema.schema as ArrayFieldSchema).type;
|
||||
return subType ? `Array<${getSubType(subType)}>` : 'Array';
|
||||
}
|
||||
case FormItemSchemaType.BOT:
|
||||
return '';
|
||||
default:
|
||||
return formSchema.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* placeholder
|
||||
* - bot:请选择bot
|
||||
* - 其他:xx必填
|
||||
*/
|
||||
export function getPlaceholder({ name, type }: FormItemSchema) {
|
||||
if (type === FormItemSchemaType.BOT) {
|
||||
return I18n.t('workflow_testset_vardatabase_placeholder');
|
||||
} else if (type === FormItemSchemaType.BOOLEAN) {
|
||||
return I18n.t('workflow_testset_please_select');
|
||||
}
|
||||
|
||||
return I18n.t('workflow_detail_title_testrun_error_input', {
|
||||
a: name || '',
|
||||
});
|
||||
}
|
||||
|
||||
/** 字段在表单中的唯一字段名 */
|
||||
export function getSubFieldName(
|
||||
formSchema: NodeFormSchema,
|
||||
itemSchema: FormItemSchema,
|
||||
) {
|
||||
return `${itemSchema.name}_${formSchema.component_id}`;
|
||||
}
|
||||
|
||||
enum VariableTypeDTO {
|
||||
object = 'object',
|
||||
list = 'list',
|
||||
string = 'string',
|
||||
integer = 'integer',
|
||||
float = 'float',
|
||||
number = 'number',
|
||||
boolean = 'boolean',
|
||||
}
|
||||
|
||||
const VariableType2JsonSchemaProps = {
|
||||
[VariableTypeDTO.object]: { type: 'object' },
|
||||
[VariableTypeDTO.list]: { type: 'array' },
|
||||
[VariableTypeDTO.float]: { type: 'number' },
|
||||
[VariableTypeDTO.number]: { type: 'number' },
|
||||
[VariableTypeDTO.integer]: { type: 'integer' },
|
||||
[VariableTypeDTO.boolean]: { type: 'boolean' },
|
||||
[VariableTypeDTO.string]: { type: 'string' },
|
||||
};
|
||||
|
||||
function workflowJsonToJsonSchema(workflowJson: any) {
|
||||
const { type, description } = workflowJson;
|
||||
const props = VariableType2JsonSchemaProps[type];
|
||||
if (type === VariableTypeDTO.object) {
|
||||
const properties = {};
|
||||
const required: string[] = [];
|
||||
for (const field of workflowJson.schema) {
|
||||
properties[field.name] = workflowJsonToJsonSchema(field);
|
||||
if (field.required) {
|
||||
required.push(field.name);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...props,
|
||||
description,
|
||||
required,
|
||||
properties,
|
||||
};
|
||||
} else if (type === VariableTypeDTO.list) {
|
||||
return {
|
||||
...props,
|
||||
description,
|
||||
items: workflowJsonToJsonSchema(workflowJson.schema),
|
||||
};
|
||||
}
|
||||
return { ...props, description };
|
||||
}
|
||||
|
||||
function validateByJsonSchema(val: any, jsonSchema: any) {
|
||||
if (!jsonSchema || !val) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ajv) {
|
||||
ajv = new Ajv();
|
||||
}
|
||||
|
||||
try {
|
||||
const validate = ajv.compile(jsonSchema);
|
||||
const valid = validate(JSON.parse(val));
|
||||
|
||||
return valid;
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch -- no-catch
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义表单的额外参数
|
||||
* 目前只对array和object表单加jsonSchema校验
|
||||
*/
|
||||
export function getCustomProps(formItemSchema: FormItemSchema) {
|
||||
switch (formItemSchema.type) {
|
||||
case FormItemSchemaType.LIST:
|
||||
case FormItemSchemaType.OBJECT: {
|
||||
const jsonSchema = workflowJsonToJsonSchema(formItemSchema);
|
||||
|
||||
return {
|
||||
trigger: ['blur'],
|
||||
jsonSchema,
|
||||
rules: [
|
||||
{
|
||||
validator: (_rules, v, cb) => {
|
||||
if (formItemSchema.required && !v) {
|
||||
cb(
|
||||
I18n.t('workflow_testset_required_tip', {
|
||||
param_name: formItemSchema.name,
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateByJsonSchema(v, jsonSchema)) {
|
||||
cb(I18n.t('workflow_debug_wrong_json'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
] as RuleItem[],
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export enum ValuesForBoolSelect {
|
||||
TRUE = 'true',
|
||||
FALSE = 'false',
|
||||
UNDEFINED = 'undefined',
|
||||
}
|
||||
|
||||
/** 布尔类型选项 */
|
||||
export const optionsForBoolSelect = [
|
||||
{
|
||||
value: ValuesForBoolSelect.TRUE,
|
||||
label: 'true',
|
||||
},
|
||||
{
|
||||
value: ValuesForBoolSelect.FALSE,
|
||||
label: 'false',
|
||||
},
|
||||
];
|
||||
|
||||
export function transBoolSelect2Bool(val?: ValuesForBoolSelect) {
|
||||
switch (val) {
|
||||
case ValuesForBoolSelect.TRUE:
|
||||
return true;
|
||||
case ValuesForBoolSelect.FALSE:
|
||||
return false;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function transBool2BoolSelect(val?: boolean) {
|
||||
switch (val) {
|
||||
case true:
|
||||
return ValuesForBoolSelect.TRUE;
|
||||
case false:
|
||||
return ValuesForBoolSelect.FALSE;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function transFormItemSchema2Form(ipt?: FormItemSchema) {
|
||||
if (ipt?.type === FormItemSchemaType.BOOLEAN) {
|
||||
return {
|
||||
...ipt,
|
||||
value: transBool2BoolSelect(ipt.value as boolean | undefined),
|
||||
};
|
||||
}
|
||||
|
||||
return ipt;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// override bad semi-select style
|
||||
/* stylelint-disable-next-line plugin/disallow-first-level-global */
|
||||
:global(.semi-popover-wrapper) {
|
||||
&:has(.dropdown) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 154px;
|
||||
padding: 0 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-usage-fill-color-fill-0, rgb(46 46 56 / 4%));
|
||||
border-color: var(--light-usage-primary-color-primary, #4D53E8);
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-select-option {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.incompatible-option {
|
||||
cursor: not-allowed;
|
||||
color: var(--semi-color-disabled-text);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
overflow: hidden;
|
||||
width: 280px;
|
||||
padding: 4px;
|
||||
|
||||
:global {
|
||||
.semi-select-option-selected .semi-select-option-icon {
|
||||
color: #4D53E8;
|
||||
}
|
||||
|
||||
.semi-select-option-list {
|
||||
/* stylelint-disable-next-line declaration-no-important -- semi-select-option-list的max-height写在了style上,所以要important覆盖 */
|
||||
max-height: 208px !important;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.semi-select-option {
|
||||
padding: 7px 16px;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(0 0 0 / 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* 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 CSSProperties, useState, useRef, useEffect } from 'react';
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { useInViewport } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
type RenderSingleSelectedItemFn,
|
||||
type SelectProps,
|
||||
} from '@coze-arch/bot-semi/Select';
|
||||
import { Select, UIModal } from '@coze-arch/bot-semi';
|
||||
import { IconSearchInput } from '@coze-arch/bot-icons';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import {
|
||||
TestsetEditSideSheet,
|
||||
type TestsetEditState,
|
||||
} from '../testset-edit-sidesheet';
|
||||
import { AutoLoadMore } from '../auto-load-more';
|
||||
import { type TestsetData } from '../../types';
|
||||
import { useTestsetManageStore, useTestsetOptions } from '../../hooks';
|
||||
import {
|
||||
SelectedTestsetOptionItem,
|
||||
TestsetOptionItem,
|
||||
} from './testset-option-item';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface TestsetSelectProps {
|
||||
/** 当前testset */
|
||||
testset: TestsetData | undefined;
|
||||
placeholder?: string;
|
||||
/** 是否有workflow编辑权限,也挂放在外层的 TestsetManageProvider上,组件上的editable优先级更高 */
|
||||
editable?: boolean;
|
||||
/** 编辑面板mask */
|
||||
editSideSheetMask?: boolean;
|
||||
onSelect: (v?: TestsetData) => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const DEBOUNCE_DELAY = 200;
|
||||
|
||||
/** option key, 更新 name、incompatible、input时都要重新渲染 */
|
||||
function getOptionKey({ caseBase, schemaIncompatible }: TestsetData) {
|
||||
return `${caseBase?.caseID}_${caseBase?.name}_${caseBase?.input}_${
|
||||
schemaIncompatible ? 0 : 1
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Testset下拉选择组件
|
||||
* 需配合`TestsetManageProvider`一起使用
|
||||
* @example
|
||||
* ``` tsx
|
||||
* <TestsetManageProvider
|
||||
* // 一些必填参数 bizCtx bizComponentSubject editable formRenders
|
||||
* >
|
||||
* <TestsetSideSheet visible={visible} onClose={() => setVisible(false)} />
|
||||
* </TestsetManageProvider>
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function -- complicated!
|
||||
export function TestsetSelect({
|
||||
testset,
|
||||
placeholder = I18n.t('workflow_debug_testset_placeholder'),
|
||||
editable,
|
||||
editSideSheetMask = false,
|
||||
className,
|
||||
style,
|
||||
onSelect,
|
||||
}: TestsetSelectProps) {
|
||||
const { editable: editableInStore, bizCtx } = useTestsetManageStore(
|
||||
store => store,
|
||||
);
|
||||
const innerEditable = editable ?? editableInStore;
|
||||
const selectRef = useRef<Select>(null);
|
||||
const editRef = useRef<boolean>(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
const [loadMoreInView] = useInViewport(loadMoreRef.current);
|
||||
const {
|
||||
loading,
|
||||
loadOptions,
|
||||
loadingMore,
|
||||
loadMoreOptions,
|
||||
optionsData,
|
||||
updateOption,
|
||||
} = useTestsetOptions();
|
||||
const [testsetEditState, setTestsetEditState] = useState<TestsetEditState>(
|
||||
{},
|
||||
);
|
||||
|
||||
// 首次加载
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setPending(true);
|
||||
const list = await loadOptions();
|
||||
if (list.length) {
|
||||
setPending(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!optionsData.hasNext || !loadMoreInView || loading || loadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadMoreOptions();
|
||||
}, [loadMoreInView]);
|
||||
|
||||
const onSearch = (input: string) => {
|
||||
loadOptions(input);
|
||||
};
|
||||
|
||||
const onEditTestset = (data: TestsetData) => {
|
||||
editRef.current = true;
|
||||
if (!innerEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectRef.current?.close();
|
||||
|
||||
setTestsetEditState({ visible: true, testset: data, mode: 'edit' });
|
||||
};
|
||||
|
||||
const onEditTestsetSuccess = (val?: TestsetData) => {
|
||||
// 1. check if selected
|
||||
if (val?.caseBase?.caseID === testset?.caseBase?.caseID) {
|
||||
// onChange new one
|
||||
onSelect(val);
|
||||
}
|
||||
// 2. refresh list
|
||||
updateOption(val);
|
||||
// 3. close edit side sheet
|
||||
closeTestsetEdit();
|
||||
};
|
||||
|
||||
// 选中Testset
|
||||
const onSelectTestset = (val: SelectProps['value']) => {
|
||||
if (typeof val !== 'string' || editRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTestset = optionsData.list.find(
|
||||
op => op.caseBase?.caseID === val,
|
||||
);
|
||||
|
||||
// 不兼容的不可选中
|
||||
if (!selectedTestset || selectedTestset.schemaIncompatible) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(selectedTestset);
|
||||
};
|
||||
|
||||
const onDeleteTestset = (data: TestsetData) => {
|
||||
editRef.current = true;
|
||||
if (!innerEditable || !data.caseBase?.caseID) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectRef.current?.close();
|
||||
|
||||
const deleteId = data.caseBase?.caseID;
|
||||
const deleteTestset = async () => {
|
||||
editRef.current = false;
|
||||
// 1. request to delete
|
||||
await debuggerApi.DeleteCaseData({
|
||||
bizCtx,
|
||||
caseIDs: [deleteId],
|
||||
});
|
||||
// 2. check if selected
|
||||
if (deleteId === testset?.caseBase?.caseID) {
|
||||
onSelect(undefined);
|
||||
}
|
||||
// 3. reload list
|
||||
await loadOptions();
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
editRef.current = false;
|
||||
};
|
||||
|
||||
UIModal.error({
|
||||
title: I18n.t('workflow_testset_delete_title'),
|
||||
content: I18n.t('workflow_testset_delete_tip'),
|
||||
cancelText: I18n.t('workflow_testset_delete_cancel'),
|
||||
okText: I18n.t('workflow_testset_delete_confirm'),
|
||||
onOk: deleteTestset,
|
||||
onCancel: cancelDelete,
|
||||
});
|
||||
};
|
||||
|
||||
const closeTestsetEdit = () => {
|
||||
editRef.current = false;
|
||||
setTestsetEditState({});
|
||||
};
|
||||
|
||||
const onDropdownVisibleChange = (visible: boolean) => {
|
||||
if (visible) {
|
||||
loadOptions();
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义选中选项
|
||||
const renderSelectedItem: RenderSingleSelectedItemFn = () =>
|
||||
testset ? <SelectedTestsetOptionItem data={testset} /> : null;
|
||||
|
||||
// testset为空的时候,不展示下拉选项(对大部分来说可能不需要看到这个下拉)
|
||||
if (pending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className={cls(s.select, className)}
|
||||
dropdownClassName={s.dropdown}
|
||||
style={style}
|
||||
prefix={<IconSearchInput className={s.prefix} />}
|
||||
filter={true}
|
||||
value={testset?.caseBase?.caseID}
|
||||
remote={true}
|
||||
onDropdownVisibleChange={onDropdownVisibleChange}
|
||||
onSelect={onSelectTestset}
|
||||
emptyContent={I18n.t('workflow_testset_search_empty')}
|
||||
placeholder={placeholder}
|
||||
ref={selectRef}
|
||||
onSearch={debounce(onSearch, DEBOUNCE_DELAY)}
|
||||
innerBottomSlot={
|
||||
<div
|
||||
ref={loadMoreRef}
|
||||
className={cls({ hidden: !optionsData.hasNext })}
|
||||
>
|
||||
<AutoLoadMore noMore={false} loadingMore={true} />
|
||||
</div>
|
||||
}
|
||||
renderSelectedItem={renderSelectedItem}
|
||||
>
|
||||
{optionsData.list.map(data => (
|
||||
<Select.Option
|
||||
value={data.caseBase?.caseID}
|
||||
// disabled的option编辑/删除唤起其他浮层后,select不会自动失焦
|
||||
// 用样式模拟disabled,并修改onSelect选中不兼容testset的逻辑
|
||||
className={cls(data.schemaIncompatible && s['incompatible-option'])}
|
||||
key={getOptionKey(data)}
|
||||
>
|
||||
<TestsetOptionItem
|
||||
data={data}
|
||||
editable={innerEditable}
|
||||
onEdit={() => onEditTestset(data)}
|
||||
onDelete={() => onDeleteTestset(data)}
|
||||
/>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<TestsetEditSideSheet
|
||||
{...testsetEditState}
|
||||
mask={editSideSheetMask}
|
||||
onSuccess={onEditTestsetSuccess}
|
||||
onClose={closeTestsetEdit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 220px;
|
||||
max-width: 220px;
|
||||
|
||||
.action {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
&:hover .action {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 18px;
|
||||
color: inherit;
|
||||
|
||||
&.incompatible {
|
||||
color: var(--light-usage-text-color-text-3, rgb(29 28 35 / 35%));
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
|
||||
display: inline-block;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--light-usage-fill-color-fill-1, rgb(46 46 56 / 8%));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 MouseEvent, type CSSProperties } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip, Typography } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconDeleteOutline,
|
||||
IconEdit,
|
||||
IconWarningInfo,
|
||||
} from '@coze-arch/bot-icons';
|
||||
|
||||
import { type TestsetData } from '../../types';
|
||||
|
||||
import s from './testset-option-item.module.less';
|
||||
|
||||
interface TestsetOptionItemProps {
|
||||
className?: string;
|
||||
data: TestsetData;
|
||||
/** 有编辑权限 */
|
||||
editable?: boolean;
|
||||
onEdit?: (data: TestsetData) => void;
|
||||
onDelete?: (data: TestsetData) => void;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
/** 多行文本展示优化 */
|
||||
const MULTILINE_TOOLTIP_STYLE: CSSProperties = { wordBreak: 'break-word' };
|
||||
|
||||
export function TestsetOptionItem({
|
||||
data,
|
||||
editable,
|
||||
className,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: TestsetOptionItemProps) {
|
||||
const incompatible = data.schemaIncompatible;
|
||||
const testsetName = data.caseBase?.name ?? '-';
|
||||
|
||||
const onOptionClick = (evt: MouseEvent<HTMLDivElement>) => {
|
||||
// 非兼容时需要阻止冒泡
|
||||
if (incompatible) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const onEditAction = () => {
|
||||
onEdit?.(data);
|
||||
};
|
||||
|
||||
const onDeleteAction = () => {
|
||||
onDelete?.(data);
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<div className={s.text} onClick={onOptionClick}>
|
||||
<Text
|
||||
className={cls(s.name, incompatible && s.incompatible)}
|
||||
ellipsis={{
|
||||
showTooltip: incompatible
|
||||
? false
|
||||
: {
|
||||
opts: {
|
||||
position: 'left',
|
||||
spacing: 48,
|
||||
content: testsetName,
|
||||
style: MULTILINE_TOOLTIP_STYLE,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{testsetName}
|
||||
</Text>
|
||||
</div>
|
||||
{editable ? (
|
||||
<div className={s.action}>
|
||||
<div role="button" className={s['action-btn']} onClick={onEditAction}>
|
||||
<IconEdit />
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
className={s['action-btn']}
|
||||
onClick={onDeleteAction}
|
||||
>
|
||||
<IconDeleteOutline />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
return incompatible ? (
|
||||
<Tooltip
|
||||
position="left"
|
||||
spacing={48}
|
||||
content={I18n.t('workflow_testset_invalid_tip', { testsetName })}
|
||||
>
|
||||
<div className={cls(s.container, className)}>
|
||||
<IconWarningInfo className={s.warning} />
|
||||
{renderContent()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className={cls(s.container, className)}>{renderContent()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 选中的回填项 */
|
||||
export function SelectedTestsetOptionItem({
|
||||
data,
|
||||
className,
|
||||
}: Pick<TestsetOptionItemProps, 'data' | 'className'>) {
|
||||
const testsetName = data.caseBase?.name || '';
|
||||
const incompatible = data.schemaIncompatible;
|
||||
const invalidTip = incompatible
|
||||
? I18n.t('workflow_testset_invaild_tip', { testset_name: testsetName })
|
||||
: undefined;
|
||||
|
||||
const renderContent = () => (
|
||||
<div className={cls(s.selected, className)}>
|
||||
{incompatible ? <IconWarningInfo className={s.warning} /> : null}
|
||||
<div className={s.text}>
|
||||
<Text
|
||||
className={cls(s.name, incompatible && s.incompatible)}
|
||||
ellipsis={{
|
||||
showTooltip: incompatible
|
||||
? false
|
||||
: {
|
||||
opts: {
|
||||
content: data.caseBase?.name,
|
||||
style: MULTILINE_TOOLTIP_STYLE,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{testsetName}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return incompatible ? (
|
||||
<Tooltip
|
||||
content={invalidTip}
|
||||
style={MULTILINE_TOOLTIP_STYLE}
|
||||
clickToHide={true}
|
||||
>
|
||||
{renderContent()}
|
||||
</Tooltip>
|
||||
) : (
|
||||
renderContent()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
.sidesheet {
|
||||
.loading-wrapper {
|
||||
width: 100%;
|
||||
height: 99%;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-sidesheet-body {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-sidesheet-content {
|
||||
background-color: var(--light-color-grey-grey-0, #F7F7FA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
transform: translateY(-15%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.container {
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 0 24px 12px;
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useInfiniteScroll } from 'ahooks';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
Empty,
|
||||
UIButton,
|
||||
Spin,
|
||||
SideSheet,
|
||||
UIToast,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
import {
|
||||
IllustrationNoContent,
|
||||
IllustrationNoContentDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
|
||||
import {
|
||||
TestsetEditSideSheet,
|
||||
type TestsetEditState,
|
||||
} from '../testset-edit-sidesheet';
|
||||
import { SideSheetTitle } from '../sidesheet-title';
|
||||
import { AutoLoadMore } from '../auto-load-more';
|
||||
import type { TestsetData } from '../../types';
|
||||
import {
|
||||
SchemaError,
|
||||
useCheckSchema,
|
||||
useTestsetManageStore,
|
||||
} from '../../hooks';
|
||||
import { TestsetListItem } from './testset-list-item';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface TestsetSideSheetProps {
|
||||
visible: boolean;
|
||||
editable?: boolean;
|
||||
onClose: () => void;
|
||||
/** 是否为多人协作模式 */
|
||||
isExpertMode?: boolean;
|
||||
}
|
||||
|
||||
interface EmptyContentProps {
|
||||
onCreateTestset?: () => void;
|
||||
}
|
||||
|
||||
function EmptyContent({ onCreateTestset }: EmptyContentProps) {
|
||||
return (
|
||||
<div className={s['empty-container']}>
|
||||
<Empty
|
||||
title={I18n.t('workflow_testset_empty')}
|
||||
className={s.empty}
|
||||
description={I18n.t('workflow_testset_create_tip')}
|
||||
image={<IllustrationNoContent />}
|
||||
darkModeImage={<IllustrationNoContentDark />}
|
||||
>
|
||||
<div className="text-center">
|
||||
<UIButton theme="solid" onClick={onCreateTestset}>
|
||||
{I18n.t('workflow_testset_create_btn')}
|
||||
</UIButton>
|
||||
</div>
|
||||
</Empty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TestsetQueryResult {
|
||||
list: TestsetData[];
|
||||
hasNext?: boolean;
|
||||
nextToken?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 30;
|
||||
|
||||
/**
|
||||
* Testset管理侧边面板
|
||||
* 需配合`TestsetManageProvider`一起使用
|
||||
*
|
||||
* @example
|
||||
* ``` tsx
|
||||
* <TestsetManageProvider
|
||||
* // 一些必填参数 bizCtx bizComponentSubject editable formRenders
|
||||
* >
|
||||
* <TestsetSideSheet visible={visible} onClose={() => setVisible(false)} />
|
||||
* </TestsetManageProvider>
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function -- 大组件>150行,只超了不到5行哈
|
||||
export function TestsetSideSheet({
|
||||
visible,
|
||||
onClose,
|
||||
isExpertMode,
|
||||
}: TestsetSideSheetProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { schemaError, checking, checkSchema } = useCheckSchema();
|
||||
const { bizComponentSubject, bizCtx } = useTestsetManageStore(store => store);
|
||||
const {
|
||||
data: testsetResp,
|
||||
loading,
|
||||
loadingMore,
|
||||
reload: reloadTestsetList,
|
||||
mutate: patchTestsetResp,
|
||||
noMore,
|
||||
} = useInfiniteScroll<TestsetQueryResult>(
|
||||
async d => {
|
||||
const res = await debuggerApi.MGetCaseData({
|
||||
bizCtx,
|
||||
bizComponentSubject,
|
||||
pageLimit: DEFAULT_PAGE_SIZE,
|
||||
nextToken: d?.nextToken,
|
||||
});
|
||||
|
||||
return {
|
||||
list: res.cases ?? [],
|
||||
hasNext: res.hasNext,
|
||||
nextToken: res.nextToken,
|
||||
};
|
||||
},
|
||||
{ target: containerRef, isNoMore: d => !d?.hasNext, manual: true },
|
||||
);
|
||||
const [testsetEditState, setTestsetEditState] = useState<TestsetEditState>(
|
||||
{},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
patchTestsetResp({ list: [] });
|
||||
reloadTestsetList();
|
||||
// 检查schema
|
||||
checkSchema();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const onCreateTestset = () => {
|
||||
if (checking) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (schemaError) {
|
||||
UIToast.error({
|
||||
content:
|
||||
schemaError === SchemaError.EMPTY
|
||||
? I18n.t('workflow_testset_paramempty')
|
||||
: I18n.t('workflow_test_nodeerror'),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setTestsetEditState({ visible: true, mode: 'create' });
|
||||
};
|
||||
|
||||
const onEditTestset = (data: TestsetData) => {
|
||||
if (checking) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (schemaError) {
|
||||
UIToast.error({
|
||||
content:
|
||||
schemaError === SchemaError.EMPTY
|
||||
? I18n.t('workflow_testset_peedit')
|
||||
: I18n.t('workflow_test_nodeerror'),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setTestsetEditState({ visible: true, testset: data, mode: 'edit' });
|
||||
};
|
||||
|
||||
const onDeleteTestset = async (data: TestsetData) => {
|
||||
if (!data.caseBase?.caseID) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await debuggerApi.DeleteCaseData({
|
||||
bizCtx,
|
||||
caseIDs: [data.caseBase?.caseID],
|
||||
});
|
||||
patchTestsetResp({ list: [] });
|
||||
reloadTestsetList();
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTestsetEdit = () => {
|
||||
setTestsetEditState({});
|
||||
};
|
||||
|
||||
const onEditSuccess = () => {
|
||||
patchTestsetResp({ list: [] });
|
||||
reloadTestsetList();
|
||||
closeTestsetEdit();
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Spin
|
||||
spinning={loading}
|
||||
tip={I18n.t('loading')}
|
||||
wrapperClassName={s['loading-wrapper']}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!testsetResp?.list.length) {
|
||||
return <EmptyContent onCreateTestset={onCreateTestset} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{testsetResp.list.map((data, i) => (
|
||||
<TestsetListItem
|
||||
key={data.caseBase?.caseID ?? i}
|
||||
data={data}
|
||||
onEdit={onEditTestset}
|
||||
onDelete={onDeleteTestset}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Testset管理侧边面板 */}
|
||||
<SideSheet
|
||||
className={s.sidesheet}
|
||||
title={
|
||||
<SideSheetTitle
|
||||
title={I18n.t('workflow_testset_tilte')}
|
||||
action={
|
||||
loading || !testsetResp?.list.length ? null : (
|
||||
<UIButton theme="solid" onClick={onCreateTestset}>
|
||||
{I18n.t('workflow_testset_create_btn')}
|
||||
</UIButton>
|
||||
)
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
}
|
||||
visible={visible}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
closable={false}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<div className={s.container} ref={containerRef}>
|
||||
{renderContent()}
|
||||
<AutoLoadMore noMore={noMore} loadingMore={loadingMore} />
|
||||
</div>
|
||||
</SideSheet>
|
||||
{/* Testset创建/编辑侧边面板 */}
|
||||
<TestsetEditSideSheet
|
||||
{...testsetEditState}
|
||||
mask={false}
|
||||
onSuccess={onEditSuccess}
|
||||
onClose={closeTestsetEdit}
|
||||
onCancel={closeTestsetEdit}
|
||||
isExpertMode={isExpertMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--light-usage-border-color-border, rgb(29 28 35 / 8%));
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-usage-fill-color-fill-1, rgb(46 46 56 / 8%));
|
||||
border-top-color: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.pressing {
|
||||
background-color: var(--light-usage-fill-color-fill-2, rgb(46 46 56 / 12%));
|
||||
border-top-color: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&:hover+&,
|
||||
&.pressing+& {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--light-usage-text-color-text-0, #1C1D23);
|
||||
}
|
||||
|
||||
.action {
|
||||
&:global(.semi-button-borderless:not(.semi-button-disabled):hover) {
|
||||
background-color: var(--semi-color-fill-1);
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 2px 0;
|
||||
font-weight: 400;
|
||||
color: rgb(29 28 35 / 80%);
|
||||
}
|
||||
|
||||
.editor-info {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-info>span,
|
||||
.editor-info .editor-info-name {
|
||||
cursor: default;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--light-usage-text-color-text-3, rgb(28 29 35 / 35%));
|
||||
}
|
||||
|
||||
.editor-info-avatar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 4px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.editor-info-separator {
|
||||
display: inline-block;
|
||||
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
margin: 0 8px;
|
||||
|
||||
background-color: var(--semi-color-border);
|
||||
}
|
||||
|
||||
.popconfirm {
|
||||
width: 315px;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 MouseEvent, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
Popconfirm,
|
||||
Tooltip,
|
||||
Typography,
|
||||
UIButton,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconDeleteOutline,
|
||||
IconEdit,
|
||||
IconWarningInfo,
|
||||
IconWaringRed,
|
||||
} from '@coze-arch/bot-icons';
|
||||
|
||||
import { type TestsetData } from '../../types';
|
||||
|
||||
import s from './testset-list-item.module.less';
|
||||
|
||||
interface TestsetListItemProps {
|
||||
data: TestsetData;
|
||||
onEdit?: (data: TestsetData) => void;
|
||||
/** 点击了删除 */
|
||||
onClickDelete?: () => void;
|
||||
/** 确认删除 */
|
||||
onDelete?: (data: TestsetData) => Promise<void>;
|
||||
}
|
||||
|
||||
function formatTime(time: unknown) {
|
||||
const x = Number(time);
|
||||
|
||||
return isNaN(x) ? '-' : dayjs.unix(x).format('YYYY.MM.DD HH:mm');
|
||||
}
|
||||
|
||||
const { Text, Title, Paragraph } = Typography;
|
||||
|
||||
export function TestsetListItem({
|
||||
data,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: TestsetListItemProps) {
|
||||
const testsetName = data.caseBase?.name ?? '-';
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [pressing, setPressing] = useState(false);
|
||||
|
||||
const onMouseDown = () => {
|
||||
setPressing(true);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
setPressing(false);
|
||||
};
|
||||
|
||||
const onContainerClick = () => {
|
||||
onEdit?.(data);
|
||||
};
|
||||
|
||||
const onClickEdit = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(data);
|
||||
};
|
||||
|
||||
const onClickDelete = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const onDeleteTestset = async () => {
|
||||
if (deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleting(true);
|
||||
try {
|
||||
await onDelete?.(data);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(s.container, pressing && s.pressing)}
|
||||
onClick={onContainerClick}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
>
|
||||
<div className="min-w-0 grow">
|
||||
<div className={s.title}>
|
||||
{data.schemaIncompatible ? (
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={I18n.t('workflow_testset_invalid_tip', { testsetName })}
|
||||
>
|
||||
<IconWarningInfo className={s.warning} />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Title
|
||||
heading={6}
|
||||
ellipsis={{ showTooltip: true }}
|
||||
className="min-w-0 grow"
|
||||
>
|
||||
{testsetName}
|
||||
</Title>
|
||||
</div>
|
||||
<Paragraph
|
||||
size="small"
|
||||
ellipsis={{ showTooltip: true }}
|
||||
className={s.desc}
|
||||
>
|
||||
{data.caseBase?.description}
|
||||
</Paragraph>
|
||||
<Text size="small" className={s['editor-info']}>
|
||||
<img
|
||||
className={s['editor-info-avatar']}
|
||||
src={data.updater?.avatarUrl}
|
||||
/>
|
||||
<Typography.Text
|
||||
className={s['editor-info-name']}
|
||||
ellipsis={{ rows: 1, showTooltip: true }}
|
||||
>
|
||||
{data.updater?.name}
|
||||
</Typography.Text>
|
||||
|
||||
<span className={s['editor-info-separator']} />
|
||||
<span>
|
||||
{`${I18n.t('workflow_testset_edited')} ${formatTime(
|
||||
data.updateTimeInSec || data.createTimeInSec,
|
||||
)}`}
|
||||
</span>
|
||||
</Text>
|
||||
</div>
|
||||
<div className="ml-4 shrink-0">
|
||||
<UIButton
|
||||
theme="borderless"
|
||||
className={s.action}
|
||||
icon={<IconEdit />}
|
||||
onClick={onClickEdit}
|
||||
/>
|
||||
<Popconfirm
|
||||
trigger="click"
|
||||
className={s.popconfirm}
|
||||
icon={<IconWaringRed />}
|
||||
title={I18n.t('workflow_testset_delete_title')}
|
||||
content={I18n.t('workflow_testset_delete_tip')}
|
||||
okText={I18n.t('workflow_testset_delete_confirm')}
|
||||
cancelText={I18n.t('workflow_testset_delete_cancel')}
|
||||
okType="danger"
|
||||
okButtonProps={{ loading: deleting }}
|
||||
onConfirm={onDeleteTestset}
|
||||
>
|
||||
<UIButton
|
||||
theme="borderless"
|
||||
className={s.action}
|
||||
icon={<IconDeleteOutline />}
|
||||
onClick={onClickDelete}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
frontend/packages/devops/testset-manage/src/context.tsx
Normal file
41
frontend/packages/devops/testset-manage/src/context.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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, type PropsWithChildren, createContext } from 'react';
|
||||
|
||||
import { createTestsetManageStore, type TestsetManageState } from './store';
|
||||
|
||||
type TestsetManageStore = ReturnType<typeof createTestsetManageStore>;
|
||||
|
||||
export const TestsetManageContext = createContext<TestsetManageStore | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function TestsetManageProvider({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<TestsetManageState>) {
|
||||
const storeRef = useRef<TestsetManageStore>();
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createTestsetManageStore(props);
|
||||
}
|
||||
|
||||
return (
|
||||
<TestsetManageContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</TestsetManageContext.Provider>
|
||||
);
|
||||
}
|
||||
22
frontend/packages/devops/testset-manage/src/events.ts
Normal file
22
frontend/packages/devops/testset-manage/src/events.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 enum TestsetManageEventName {
|
||||
/** 创建测试集成功 */
|
||||
CREATE_TESTSET_SUCCESS = 'create_testset_success',
|
||||
/** 点击AI生成节点入参 */
|
||||
AIGC_PARAMS_CLICK = 'aigc_params_click',
|
||||
}
|
||||
19
frontend/packages/devops/testset-manage/src/hooks/index.ts
Normal file
19
frontend/packages/devops/testset-manage/src/hooks/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { useTestsetOptions } from './use-testset-options';
|
||||
export { useTestsetManageStore } from './use-testset-manage-store';
|
||||
export { useCheckSchema, SchemaError } from './use-check-schema';
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { ComponentType } from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import {
|
||||
type ArrayFieldSchema,
|
||||
type FormItemSchema,
|
||||
FormItemSchemaType,
|
||||
type ObjectFieldSchema,
|
||||
type NodeFormSchema,
|
||||
} from '../types';
|
||||
import { useTestsetManageStore } from './use-testset-manage-store';
|
||||
|
||||
export enum SchemaError {
|
||||
OK = '',
|
||||
EMPTY = 'empty',
|
||||
INVALID = 'invalid',
|
||||
}
|
||||
|
||||
/** 变量命名校验规则(对齐workflow得参数名校验) */
|
||||
const PARAM_NAME_VALIDATION_RULE =
|
||||
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/;
|
||||
|
||||
function validateParamName(name?: string) {
|
||||
return Boolean(name && PARAM_NAME_VALIDATION_RULE.test(name));
|
||||
}
|
||||
|
||||
function isArrayOrObjectField(field: FormItemSchema) {
|
||||
return (
|
||||
field.type === FormItemSchemaType.LIST ||
|
||||
field.type === FormItemSchemaType.OBJECT
|
||||
);
|
||||
}
|
||||
|
||||
function validateArrayOrObjectSchema(
|
||||
schema?: ObjectFieldSchema | ArrayFieldSchema,
|
||||
) {
|
||||
if (!schema) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
const nameSet = new Set<string>();
|
||||
for (const sub of schema) {
|
||||
if (!validateParamName(sub.name) || nameSet.has(sub.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nameSet.add(sub.name);
|
||||
|
||||
if (
|
||||
isArrayOrObjectField(sub) &&
|
||||
!validateArrayOrObjectSchema(sub.schema)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(schema.type);
|
||||
}
|
||||
|
||||
function checkArrayOrObjectField(field: FormItemSchema) {
|
||||
if (!isArrayOrObjectField(field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!field.schema) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(field.schema)) {
|
||||
const nameSet = new Set<string>();
|
||||
for (const item of field.schema) {
|
||||
if (!validateParamName(item.name) || nameSet.has(item.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nameSet.add(item.name);
|
||||
|
||||
if (
|
||||
isArrayOrObjectField(item) &&
|
||||
!validateArrayOrObjectSchema(item.schema)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkNodeFormSchema(schema: NodeFormSchema) {
|
||||
// 节点参数为空
|
||||
if (!schema.inputs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nameSet = new Set<string>();
|
||||
for (const ipt of schema.inputs) {
|
||||
// 名称非法 or 重复
|
||||
if (!validateParamName(ipt.name) || nameSet.has(ipt.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nameSet.add(ipt.name);
|
||||
|
||||
// 单独检测复杂类型
|
||||
if (!checkArrayOrObjectField(ipt)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateSchema(json?: string) {
|
||||
if (!json) {
|
||||
return SchemaError.INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
const schemas = JSON.parse(json) as NodeFormSchema[];
|
||||
|
||||
// schema为空 or start节点的inputs为空
|
||||
const isEmpty =
|
||||
schemas.length === 0 ||
|
||||
(schemas[0].component_type === ComponentType.CozeStartNode &&
|
||||
schemas[0].inputs.length === 0);
|
||||
|
||||
if (isEmpty) {
|
||||
return SchemaError.EMPTY;
|
||||
}
|
||||
|
||||
for (const schema of schemas) {
|
||||
if (!checkNodeFormSchema(schema)) {
|
||||
return SchemaError.INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
return SchemaError.OK;
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
return SchemaError.OK;
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查workflow节点表单是否为空(schema为空 or start节点的inputs为空) */
|
||||
export function useCheckSchema() {
|
||||
const { bizComponentSubject, bizCtx } = useTestsetManageStore(store => store);
|
||||
const [schemaError, setSchemaError] = useState(SchemaError.OK);
|
||||
const [checking, setChecking] = useState(false);
|
||||
|
||||
const checkSchema = useMemoizedFn(async () => {
|
||||
setChecking(true);
|
||||
try {
|
||||
const resp = await debuggerApi.GetSchemaByID({
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
});
|
||||
const err = validateSchema(resp.schemaJson);
|
||||
|
||||
setSchemaError(err);
|
||||
return err;
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
setSchemaError(SchemaError.OK);
|
||||
return SchemaError.OK;
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
});
|
||||
|
||||
return { schemaError, checkSchema, checking };
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { useContext } from 'react';
|
||||
|
||||
import { useStore } from 'zustand';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
import { type TestsetManageProps } from '../store';
|
||||
import { TestsetManageContext } from '../context';
|
||||
|
||||
export function useTestsetManageStore<T>(
|
||||
selector: (s: TestsetManageProps) => T,
|
||||
): T {
|
||||
const store = useContext(TestsetManageContext);
|
||||
|
||||
if (!store) {
|
||||
throw new CustomError(
|
||||
'normal_error',
|
||||
'Missing TestsetManageProvider in the tree',
|
||||
);
|
||||
}
|
||||
|
||||
return useStore(store, selector);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 { useMemoizedFn } from 'ahooks';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type TestsetData } from '../types';
|
||||
import { useTestsetManageStore } from './use-testset-manage-store';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 30;
|
||||
|
||||
export interface OptionsData {
|
||||
list: TestsetData[];
|
||||
hasNext?: boolean;
|
||||
nextToken?: string;
|
||||
}
|
||||
|
||||
export function useTestsetOptions() {
|
||||
const { bizComponentSubject, bizCtx } = useTestsetManageStore(store => store);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [optionsData, setOptionsData] = useState<OptionsData>({ list: [] });
|
||||
|
||||
const updateOption = useMemoizedFn((testset?: TestsetData) => {
|
||||
if (!testset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = optionsData.list.findIndex(
|
||||
v => v.caseBase?.caseID === testset.caseBase?.caseID,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
const newList = [...optionsData.list];
|
||||
newList[index] = testset;
|
||||
setOptionsData(prev => ({ ...prev, list: newList }));
|
||||
}
|
||||
});
|
||||
|
||||
const loadOptions = useMemoizedFn(
|
||||
async (q?: string, limit = DEFAULT_PAGE_SIZE) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const {
|
||||
cases = [],
|
||||
hasNext,
|
||||
nextToken,
|
||||
} = await debuggerApi.MGetCaseData({
|
||||
bizCtx,
|
||||
bizComponentSubject,
|
||||
caseName: q,
|
||||
pageLimit: limit,
|
||||
});
|
||||
setOptionsData({ list: cases, hasNext, nextToken });
|
||||
return cases;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const loadMoreOptions = useMemoizedFn(
|
||||
async (q?: string, limit = DEFAULT_PAGE_SIZE) => {
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
cases = [],
|
||||
hasNext,
|
||||
nextToken,
|
||||
} = await debuggerApi.MGetCaseData({
|
||||
bizCtx,
|
||||
bizComponentSubject,
|
||||
caseName: q,
|
||||
pageLimit: limit,
|
||||
nextToken: optionsData.nextToken,
|
||||
});
|
||||
setOptionsData(prev => ({
|
||||
list: [...prev.list, ...cases],
|
||||
hasNext,
|
||||
nextToken,
|
||||
}));
|
||||
return cases;
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
loading,
|
||||
loadOptions,
|
||||
loadingMore,
|
||||
loadMoreOptions,
|
||||
optionsData,
|
||||
updateOption,
|
||||
};
|
||||
}
|
||||
35
frontend/packages/devops/testset-manage/src/index.ts
Normal file
35
frontend/packages/devops/testset-manage/src/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 {
|
||||
TestsetSideSheet,
|
||||
type TestsetSideSheetProps,
|
||||
} from './components/testset-sidesheet';
|
||||
export {
|
||||
TestsetSelect,
|
||||
type TestsetSelectProps,
|
||||
} from './components/testset-select';
|
||||
export { getTestsetNameRules } from './components/testset-edit-sidesheet/utils';
|
||||
export { TestsetManageProvider } from './context';
|
||||
export { useTestsetManageStore, useCheckSchema } from './hooks';
|
||||
export {
|
||||
FormItemSchemaType,
|
||||
type NodeFormItem,
|
||||
type NodeFormSchema,
|
||||
type TestsetData,
|
||||
type TestsetDatabase,
|
||||
} from './types';
|
||||
export { TestsetManageEventName } from './events';
|
||||
71
frontend/packages/devops/testset-manage/src/store.ts
Normal file
71
frontend/packages/devops/testset-manage/src/store.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
type BizCtx,
|
||||
type ComponentSubject,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import { type NodeFormItem, type FormItemSchemaType } from './types';
|
||||
import { type TestsetManageEventName } from './events';
|
||||
|
||||
export interface TestsetManageState {
|
||||
bizCtx?: BizCtx;
|
||||
bizComponentSubject?: ComponentSubject;
|
||||
/** 编辑权限 */
|
||||
editable?: boolean;
|
||||
/** 表单渲染组件 */
|
||||
formRenders?: Partial<Record<FormItemSchemaType, NodeFormItem>>;
|
||||
/** 埋点事件上报 */
|
||||
reportEvent?: (
|
||||
name: TestsetManageEventName,
|
||||
params?: Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface TestsetManageAction {
|
||||
/** 更新状态 */
|
||||
patch: (s: Partial<TestsetManageState>) => void;
|
||||
}
|
||||
|
||||
export type TestsetManageProps = TestsetManageState & TestsetManageAction;
|
||||
|
||||
export function createTestsetManageStore(
|
||||
initState: Partial<TestsetManageState>,
|
||||
) {
|
||||
return create<TestsetManageProps>((set, get) => ({
|
||||
...initState,
|
||||
patch: s => {
|
||||
set(prev => ({ ...prev, ...s }));
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
interface InnerState {
|
||||
generating: boolean;
|
||||
}
|
||||
|
||||
interface InnerAction {
|
||||
patch: (s: Partial<InnerState>) => void;
|
||||
}
|
||||
|
||||
export const useInnerStore = create<InnerState & InnerAction>((set, get) => ({
|
||||
generating: false,
|
||||
patch: s => {
|
||||
set({ ...s });
|
||||
},
|
||||
}));
|
||||
70
frontend/packages/devops/testset-manage/src/types.ts
Normal file
70
frontend/packages/devops/testset-manage/src/types.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import {
|
||||
type CaseDataDetail,
|
||||
type CaseDataBase,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
export type TestsetData = CaseDataDetail;
|
||||
export type TestsetDatabase = CaseDataBase;
|
||||
|
||||
export enum FormItemSchemaType {
|
||||
STRING = 'string',
|
||||
BOT = 'bot',
|
||||
NUMBER = 'number',
|
||||
OBJECT = 'object',
|
||||
BOOLEAN = 'boolean',
|
||||
INTEGER = 'integer',
|
||||
FLOAT = 'float',
|
||||
LIST = 'list',
|
||||
}
|
||||
|
||||
export interface ArrayFieldSchema {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type ObjectFieldSchema = {
|
||||
name: string;
|
||||
type: string;
|
||||
schema?: ArrayFieldSchema | ObjectFieldSchema;
|
||||
}[];
|
||||
|
||||
export interface FormItemSchema {
|
||||
// 扩展为枚举
|
||||
type: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
value?: string | number | boolean;
|
||||
/** object/array复杂类型有schema定义 */
|
||||
schema?: ArrayFieldSchema | ObjectFieldSchema;
|
||||
}
|
||||
|
||||
export interface NodeFormSchema {
|
||||
component_id: string;
|
||||
component_type: number;
|
||||
component_name: string;
|
||||
component_icon?: string;
|
||||
inputs: FormItemSchema[];
|
||||
}
|
||||
|
||||
export interface NodeFormItem {
|
||||
(props: CommonFieldProps): ReactNode;
|
||||
}
|
||||
33
frontend/packages/devops/testset-manage/src/typing.d.ts
vendored
Normal file
33
frontend/packages/devops/testset-manage/src/typing.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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' />
|
||||
|
||||
declare module '*.less' {
|
||||
const classes: { readonly [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
export const ReactComponent: React.FunctionComponent<
|
||||
React.SVGProps<SVGSVGElement>
|
||||
>;
|
||||
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
// declare const IS_OVERSEA: boolean;
|
||||
Reference in New Issue
Block a user