feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
.collapse-title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
height: 24px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.collapse-title-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--coz-bg-plus, rgba(252, 252, 255, 1));
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
.collapse-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
height: 20px;
|
||||
color: var(--coz-fg-primary);
|
||||
}
|
||||
.collapse-icon {
|
||||
transition: transform 0.3s ease-in-out, opacity 0.2s ease-in-out;
|
||||
opacity: 0;
|
||||
color: var(--coz-fg-dim);
|
||||
font-size: 14px;
|
||||
margin: 0 1px;
|
||||
&.is-show {
|
||||
opacity: 1;
|
||||
}
|
||||
&.is-close {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-extra {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState, forwardRef } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { useHover } from 'ahooks';
|
||||
import { IconCozArrowDownFill } from '@coze-arch/coze-design/icons';
|
||||
import { Collapsible } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './collapse.module.less';
|
||||
|
||||
interface CollapseProps {
|
||||
label: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
titleSticky?: boolean;
|
||||
contentClassName?: string;
|
||||
titleClassName?: string;
|
||||
className?: string;
|
||||
extraClassName?: string;
|
||||
fade?: boolean;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export const Collapse = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.PropsWithChildren<CollapseProps>
|
||||
>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
extra,
|
||||
children,
|
||||
contentClassName,
|
||||
titleClassName,
|
||||
titleSticky,
|
||||
className,
|
||||
extraClassName,
|
||||
fade,
|
||||
duration,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const isTitleHover = useHover(() => titleRef.current);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
<div
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
ref={titleRef}
|
||||
className={cls(
|
||||
'cursor-pointer',
|
||||
styles['collapse-title'],
|
||||
{
|
||||
[styles['collapse-title-sticky']]: titleSticky,
|
||||
},
|
||||
titleClassName,
|
||||
)}
|
||||
>
|
||||
<IconCozArrowDownFill
|
||||
className={cls(
|
||||
styles['collapse-icon'],
|
||||
!isOpen && styles['is-close'],
|
||||
isTitleHover && styles['is-show'],
|
||||
)}
|
||||
/>
|
||||
<span className={styles['collapse-label']}>{label}</span>
|
||||
|
||||
{extra ? (
|
||||
<div
|
||||
className={cls(styles['collapse-extra'], extraClassName)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{extra}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Collapsible
|
||||
className={contentClassName}
|
||||
isOpen={isOpen}
|
||||
keepDOM
|
||||
fade={fade}
|
||||
duration={duration}
|
||||
>
|
||||
{children}
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Collapse } from './collapse';
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 FileItemStatus {
|
||||
Success = 'success',
|
||||
UploadFail = 'uploadFail',
|
||||
ValidateFail = 'validateFail',
|
||||
Validating = 'validating',
|
||||
Uploading = 'uploading',
|
||||
Wait = 'wait',
|
||||
}
|
||||
|
||||
// 支持预览的图片类型
|
||||
export const PREVIEW_IMAGE_TYPE = ['jpg', 'jpeg', 'png', 'webp', 'svg'];
|
||||
@@ -0,0 +1,10 @@
|
||||
.file-icon-loading {
|
||||
:global {
|
||||
.semi-spin-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 { Spin, Image } from '@coze-arch/coze-design';
|
||||
|
||||
import { getIconByExtension, getFileExtension } from './utils';
|
||||
import { FileItemStatus, PREVIEW_IMAGE_TYPE } from './constants';
|
||||
|
||||
import styles from './file-icon.module.less';
|
||||
|
||||
interface FileIconProps {
|
||||
file: {
|
||||
name: string;
|
||||
url?: string;
|
||||
status?: string;
|
||||
};
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const FileIcon = (props: FileIconProps) => {
|
||||
const { size = 20, file } = props;
|
||||
|
||||
const { url, name, status } = file;
|
||||
|
||||
const extension = getFileExtension(name);
|
||||
|
||||
if (status === FileItemStatus.Uploading) {
|
||||
return (
|
||||
<Spin
|
||||
wrapperClassName={styles['file-icon-loading']}
|
||||
style={{ width: size, height: size, lineHeight: `${size}px` }}
|
||||
spinning
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (PREVIEW_IMAGE_TYPE.includes(extension)) {
|
||||
return (
|
||||
<Image
|
||||
preview={false}
|
||||
className="object-contain object-center rounded-sm border-0"
|
||||
style={{ width: size, height: size }}
|
||||
imgStyle={{ width: size, height: size }}
|
||||
src={url}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = getIconByExtension(extension);
|
||||
|
||||
return <Icon style={{ width: size, height: size, fontSize: size }} />;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { FileIcon } from './file-icon';
|
||||
|
||||
export { FileItemStatus } from './constants';
|
||||
export { getFileExtension, isImageFile } from './utils';
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 mime from 'mime-types';
|
||||
import {
|
||||
IconCozFileAudio,
|
||||
IconCozFileCode,
|
||||
IconCozFilePptx,
|
||||
IconCozFileDocx,
|
||||
IconCozFileTxt,
|
||||
IconCozFileZip,
|
||||
IconCozFileXlsx,
|
||||
IconCozFileVideo,
|
||||
IconCozFileOther,
|
||||
IconCozFilePdf,
|
||||
IconCozFileCsv,
|
||||
} from '@coze-arch/coze-design/illustrations';
|
||||
|
||||
import { PREVIEW_IMAGE_TYPE } from './constants';
|
||||
|
||||
const codeExtensions = [
|
||||
'js',
|
||||
'jsx',
|
||||
'ts',
|
||||
'tsx',
|
||||
'html',
|
||||
'htm',
|
||||
'css',
|
||||
'scss',
|
||||
'sass',
|
||||
'less',
|
||||
'py',
|
||||
'java',
|
||||
'c',
|
||||
'cpp',
|
||||
'h',
|
||||
'hpp',
|
||||
'cs',
|
||||
'go',
|
||||
'rb',
|
||||
'php',
|
||||
'swift',
|
||||
'kt',
|
||||
'kts',
|
||||
'sql',
|
||||
'pl',
|
||||
'sh',
|
||||
'bash',
|
||||
'rs',
|
||||
'dart',
|
||||
'scala',
|
||||
'yaml',
|
||||
'yml',
|
||||
'json',
|
||||
];
|
||||
|
||||
function isCodeFile(extension: string) {
|
||||
return codeExtensions.includes(extension);
|
||||
}
|
||||
|
||||
function isAudioFile(extension: string) {
|
||||
const mimeType = mime.lookup(extension);
|
||||
return mimeType ? mimeType.startsWith('audio/') : false;
|
||||
}
|
||||
|
||||
function isVideoFile(extension: string) {
|
||||
const mimeType = mime.lookup(extension);
|
||||
return mimeType ? mimeType.startsWith('video/') : false;
|
||||
}
|
||||
|
||||
const ICON_MAP = {
|
||||
// ppt
|
||||
ppt: IconCozFilePptx,
|
||||
pptx: IconCozFilePptx,
|
||||
// doc
|
||||
doc: IconCozFileDocx,
|
||||
docx: IconCozFileDocx,
|
||||
pdf: IconCozFilePdf,
|
||||
// txt
|
||||
txt: IconCozFileTxt,
|
||||
// zip
|
||||
zip: IconCozFileZip,
|
||||
rar: IconCozFileZip,
|
||||
// excel
|
||||
xls: IconCozFileXlsx,
|
||||
xlsx: IconCozFileXlsx,
|
||||
csv: IconCozFileCsv,
|
||||
// code
|
||||
code: IconCozFileCode,
|
||||
// video
|
||||
video: IconCozFileVideo,
|
||||
// audio
|
||||
audio: IconCozFileAudio,
|
||||
};
|
||||
|
||||
export const getIconByExtension = (extension: string) => {
|
||||
let fileIcon = ICON_MAP[extension] ?? IconCozFileOther;
|
||||
if (isAudioFile(extension)) {
|
||||
fileIcon = ICON_MAP.audio;
|
||||
} else if (isVideoFile(extension)) {
|
||||
fileIcon = ICON_MAP.video;
|
||||
} else if (isCodeFile(extension)) {
|
||||
fileIcon = ICON_MAP.code;
|
||||
}
|
||||
|
||||
return fileIcon;
|
||||
};
|
||||
|
||||
/** 获取文件名后缀 */
|
||||
export function getFileExtension(name?: string) {
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1).toLowerCase();
|
||||
}
|
||||
|
||||
export const isImageFile = (name: string) => {
|
||||
const ext = getFileExtension(name);
|
||||
return PREVIEW_IMAGE_TYPE.includes(ext);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 const MAX_IMAGE_SIZE = 1024 * 1024 * 5;
|
||||
export const MAX_FILE_SIZE = 1024 * 1024 * 20;
|
||||
@@ -0,0 +1,36 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.container {
|
||||
&.hide-upload-area {
|
||||
:global {
|
||||
.semi-upload-drag-area {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-upload-drag-area {
|
||||
.semi-upload-drag-area-sub-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-upload-file-card-preview-placeholder {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.semi-upload.has-error .semi-upload-drag-area {
|
||||
border: 1px solid var(--Light-usage-danger---color-danger, #FF441E);
|
||||
}
|
||||
|
||||
.semi-upload-file-card-info-main {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.upload-error-text {
|
||||
color: var(--Light-usage-danger---color-danger, #FF441E);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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 ClickAwayListener from 'react-click-away-listener';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useUpdateEffect, useMemoizedFn } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Upload, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { FileIcon, FileItemStatus } from '../file-icon';
|
||||
import { typeSafeJSONParse } from '../../utils';
|
||||
import { formatBytes } from './utils';
|
||||
import { useUpload } from './use-upload';
|
||||
import { type FileItem } from './types';
|
||||
|
||||
import styles from './file-upload.module.less';
|
||||
|
||||
export interface FileUploadProps {
|
||||
value: string;
|
||||
accept: string;
|
||||
multiple: boolean;
|
||||
disabled?: boolean;
|
||||
fileType: 'object' | 'image';
|
||||
validateStatus?: string;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
onChange?: (v?: string) => void;
|
||||
}
|
||||
|
||||
const TEST_RUN_FILE_NAME_KEY = 'x-wf-file_name';
|
||||
|
||||
const getFormatFileUrl = (curFile: FileItem) => {
|
||||
const originUrl = curFile?.url ?? '';
|
||||
const fileName = curFile?.name ?? '';
|
||||
try {
|
||||
const urlObj = new URL(originUrl);
|
||||
const params = new URLSearchParams(urlObj.search);
|
||||
|
||||
if (params.has(TEST_RUN_FILE_NAME_KEY)) {
|
||||
params.set(TEST_RUN_FILE_NAME_KEY, fileName);
|
||||
} else {
|
||||
params.append(TEST_RUN_FILE_NAME_KEY, fileName);
|
||||
}
|
||||
|
||||
urlObj.search = params.toString();
|
||||
|
||||
return urlObj.toString();
|
||||
} catch (e) {
|
||||
return originUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const getParsedFileInfo = (formatUrl: string) => {
|
||||
const url = new URL(formatUrl);
|
||||
const params = new URLSearchParams(url.search);
|
||||
const fileName =
|
||||
params.get(TEST_RUN_FILE_NAME_KEY) ?? I18n.t('plugin_file_unknown');
|
||||
|
||||
return {
|
||||
url: formatUrl,
|
||||
uid: nanoid(),
|
||||
name: fileName,
|
||||
};
|
||||
};
|
||||
|
||||
const getInitialValue = (val: string, multiple: boolean): FileItem[] => {
|
||||
if (multiple) {
|
||||
const multipleVal = typeSafeJSONParse(val);
|
||||
if (Array.isArray(multipleVal)) {
|
||||
return multipleVal.map(url => getParsedFileInfo(url)) as FileItem[];
|
||||
}
|
||||
}
|
||||
if (val) {
|
||||
return [getParsedFileInfo(val)] as FileItem[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const FileUpload: React.FC<FileUploadProps> = props => {
|
||||
const {
|
||||
validateStatus,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
accept,
|
||||
multiple,
|
||||
disabled,
|
||||
fileType,
|
||||
} = props;
|
||||
|
||||
const focusRef = useRef(false);
|
||||
|
||||
const maxFileCount = multiple ? 20 : 1;
|
||||
|
||||
const { upload, fileList, isUploading, deleteFile, setFileList } = useUpload({
|
||||
multiple,
|
||||
fileType,
|
||||
accept,
|
||||
maxFileCount,
|
||||
});
|
||||
|
||||
const handleFocus = () => {
|
||||
if (!focusRef.current) {
|
||||
focusRef.current = true;
|
||||
onFocus?.();
|
||||
}
|
||||
};
|
||||
const handleBlur = () => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current = false;
|
||||
onBlur?.();
|
||||
}
|
||||
};
|
||||
const handleClickAway = () => {
|
||||
if (!isUploading) {
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async file => {
|
||||
const { fileInstance } = file;
|
||||
await upload(fileInstance);
|
||||
};
|
||||
|
||||
const getSubmitValue = useMemoizedFn((): string | undefined => {
|
||||
let newVal: string | undefined;
|
||||
if (multiple) {
|
||||
const next = fileList
|
||||
.filter(item => item.url)
|
||||
.map(item => getFormatFileUrl(item));
|
||||
newVal = next.length ? JSON.stringify(next) : undefined;
|
||||
} else {
|
||||
const singleFile = fileList?.[0];
|
||||
newVal = singleFile ? getFormatFileUrl(singleFile) : undefined;
|
||||
}
|
||||
return newVal;
|
||||
});
|
||||
|
||||
const handleChange = useMemoizedFn(val => onChange?.(val));
|
||||
|
||||
// 当fileList更新时,触发onChange
|
||||
useUpdateEffect(() => {
|
||||
const newVal = getSubmitValue();
|
||||
handleChange?.(newVal);
|
||||
}, [fileList]);
|
||||
|
||||
// 当表单值更新时,同步到fileList
|
||||
useEffect(() => {
|
||||
const val = getSubmitValue();
|
||||
if (val !== value) {
|
||||
setFileList(getInitialValue(value, multiple));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleAcceptInvalid = () => {
|
||||
Toast.error(
|
||||
I18n.t('imageflow_upload_error_type', {
|
||||
type: accept,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const semiFileList: any[] = fileList.map(file => ({
|
||||
name: file.name,
|
||||
size: file.size !== undefined ? formatBytes(file.size) : '',
|
||||
uid: file.uid || nanoid(),
|
||||
status: file.status || FileItemStatus.Success,
|
||||
url: file.uil,
|
||||
validateMessage: file.validateMessage,
|
||||
percent: file.percent,
|
||||
preview: true,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<div
|
||||
className={classNames(styles.container, {
|
||||
[styles['hide-upload-area']]: fileList.length >= maxFileCount,
|
||||
})}
|
||||
>
|
||||
<Upload
|
||||
disabled={disabled}
|
||||
action=""
|
||||
limit={maxFileCount}
|
||||
fileList={semiFileList}
|
||||
data-testid={props['data-testid']}
|
||||
className={validateStatus === 'error' ? 'has-error' : ''}
|
||||
customRequest={handleUpload}
|
||||
draggable={true}
|
||||
dragMainText={I18n.t('imageflow_upload_action_common')}
|
||||
dragSubText={I18n.t('imageflow_upload_type', {
|
||||
type: accept,
|
||||
})}
|
||||
multiple={multiple}
|
||||
accept={accept}
|
||||
onDrop={handleFocus}
|
||||
onOpenFileDialog={handleFocus}
|
||||
onAcceptInvalid={handleAcceptInvalid}
|
||||
previewFile={file => {
|
||||
const { uid } = file;
|
||||
const fileItem = fileList.find(item => item.uid === uid);
|
||||
if (fileItem) {
|
||||
return <FileIcon file={fileItem} size={36} />;
|
||||
}
|
||||
}}
|
||||
onRemove={(currentFile, list, currentFileItem) => {
|
||||
deleteFile(currentFileItem.uid);
|
||||
}}
|
||||
onClear={() => setFileList([])}
|
||||
/>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FileUpload, type FileUploadProps } from './file-upload';
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { FileItemStatus } from '../file-icon';
|
||||
|
||||
export interface FileItem extends File {
|
||||
// 唯一标识
|
||||
uid?: string;
|
||||
// 文件地址
|
||||
url?: string;
|
||||
// 上传进度
|
||||
percent?: number;
|
||||
// 校验信息
|
||||
validateMessage?: string;
|
||||
status?: FileItemStatus;
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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 max-lines-per-function */
|
||||
import { useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { workflowApi, type ViewVariableType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { upLoadFile } from '@coze-arch/bot-utils';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { FileItemStatus } from '../file-icon';
|
||||
import { validate } from './utils';
|
||||
import { type FileItem } from './types';
|
||||
import { MAX_IMAGE_SIZE, MAX_FILE_SIZE } from './constants';
|
||||
|
||||
export interface UploadConfig {
|
||||
initialValue?: FileItem[];
|
||||
customValidate?: (file: FileItem) => Promise<string | undefined>;
|
||||
timeout?: number;
|
||||
fileType?: 'object' | 'image';
|
||||
multiple?: boolean;
|
||||
maxSize?: number;
|
||||
inputType?: ViewVariableType;
|
||||
accept?: string;
|
||||
maxFileCount?: number;
|
||||
}
|
||||
|
||||
export const useUpload = (props?: UploadConfig) => {
|
||||
const {
|
||||
initialValue = [],
|
||||
customValidate,
|
||||
timeout,
|
||||
fileType,
|
||||
multiple = true,
|
||||
maxSize,
|
||||
accept,
|
||||
maxFileCount = 20,
|
||||
} = props || {};
|
||||
const [fileList, setFileList] = useState(initialValue);
|
||||
const isUploading = fileList.some(
|
||||
file => file.status === FileItemStatus.Uploading,
|
||||
);
|
||||
|
||||
const updateFileItemProps = (uid, fileItemProps) => {
|
||||
setFileList(prevList => {
|
||||
const newList = [...prevList];
|
||||
const index = newList.findIndex(item => item.uid === uid);
|
||||
if (index !== -1) {
|
||||
Object.keys(fileItemProps).forEach(key => {
|
||||
newList[index][key] = fileItemProps[key];
|
||||
});
|
||||
}
|
||||
|
||||
return newList;
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFileWithProgress = async file => {
|
||||
let progressTimer;
|
||||
|
||||
try {
|
||||
const doUpload = async () =>
|
||||
await upLoadFile({
|
||||
biz: 'workflow',
|
||||
fileType,
|
||||
file,
|
||||
getProgress: percent => {
|
||||
updateFileItemProps(file.uid, {
|
||||
percent,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (timeout) {
|
||||
progressTimer = setTimeout(() => {
|
||||
throw new Error('Upload timed out');
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
const uri = await doUpload();
|
||||
|
||||
if (!uri) {
|
||||
throw new CustomError('normal_error', 'no uri');
|
||||
}
|
||||
|
||||
// 上传完成,清空超时计时器
|
||||
clearTimeout(progressTimer);
|
||||
|
||||
// 加签uri,获得url
|
||||
const { url } = await workflowApi.SignImageURL(
|
||||
{
|
||||
uri,
|
||||
},
|
||||
{
|
||||
__disableErrorToast: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!url) {
|
||||
throw new Error(I18n.t('imageflow_upload_error'));
|
||||
}
|
||||
|
||||
updateFileItemProps(file.uid, {
|
||||
url,
|
||||
status: FileItemStatus.Success,
|
||||
});
|
||||
|
||||
return url;
|
||||
} catch (error) {
|
||||
updateFileItemProps(file.uid, {
|
||||
validateMessage: error.message || 'upload failed',
|
||||
status: FileItemStatus.ValidateFail,
|
||||
});
|
||||
clearTimeout(progressTimer);
|
||||
}
|
||||
};
|
||||
|
||||
const validateFile = async (file: FileItem): Promise<string | undefined> => {
|
||||
const validateMsg = await validate(file, {
|
||||
customValidate,
|
||||
maxSize:
|
||||
(maxSize ?? fileType === 'image') ? MAX_IMAGE_SIZE : MAX_FILE_SIZE,
|
||||
accept,
|
||||
});
|
||||
if (validateMsg) {
|
||||
return validateMsg;
|
||||
}
|
||||
};
|
||||
|
||||
const upload = async (file: FileItem) => {
|
||||
file.status = FileItemStatus.Uploading;
|
||||
if (!file.uid) {
|
||||
file.uid = nanoid();
|
||||
}
|
||||
|
||||
const errorInfo = await validateFile(file);
|
||||
|
||||
if (errorInfo) {
|
||||
Toast.error(errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!multiple && fileList[0]) {
|
||||
setFileList([]);
|
||||
}
|
||||
|
||||
let canUpload = true;
|
||||
|
||||
setFileList(prevList => {
|
||||
if (prevList.length >= maxFileCount) {
|
||||
Toast.warning(I18n.t('plugin_file_max'));
|
||||
canUpload = false;
|
||||
return prevList;
|
||||
}
|
||||
return [...prevList, file];
|
||||
});
|
||||
|
||||
if (canUpload) {
|
||||
await uploadFileWithProgress(file);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFile = (uid?: string) => {
|
||||
const index = fileList.findIndex(item => uid === item.uid);
|
||||
|
||||
if (index !== -1 && uid) {
|
||||
setFileList(prevList => {
|
||||
const newList = [...prevList];
|
||||
newList.splice(index, 1);
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
upload,
|
||||
isUploading,
|
||||
deleteFile,
|
||||
setFileList: _fileList => setFileList(_fileList),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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 mime from 'mime-types';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { getFileExtension } from '../file-icon';
|
||||
import { type FileItem } from './types';
|
||||
import { MAX_FILE_SIZE } from './constants';
|
||||
|
||||
interface UploadValidateRule {
|
||||
maxSize?: number;
|
||||
imageSize?: ImageSizeRule;
|
||||
accept?: string;
|
||||
customValidate?: (file: FileItem) => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param bytes 文件大小
|
||||
* @param decimals 小数位数, 默认 2 位
|
||||
* @example
|
||||
* formatBytes(1024); // 1KB
|
||||
* formatBytes('1024'); // 1KB
|
||||
* formatBytes(1234); // 1.21KB
|
||||
* formatBytes(1234, 3); // 1.205KB
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
const k = 1024,
|
||||
dm = decimals,
|
||||
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`;
|
||||
}
|
||||
|
||||
/** 文件大小校验 */
|
||||
export const sizeValidate = (
|
||||
size: number,
|
||||
maxSize: number = MAX_FILE_SIZE,
|
||||
): string | undefined => {
|
||||
if (maxSize && size > maxSize) {
|
||||
return I18n.t('imageflow_upload_exceed', {
|
||||
size: formatBytes(maxSize),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export interface ImageSizeRule {
|
||||
maxWidth?: number;
|
||||
minWidth?: number;
|
||||
maxHeight?: number;
|
||||
minHeight?: number;
|
||||
aspectRatio?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片的宽高
|
||||
*/
|
||||
export async function getImageSize(
|
||||
file: FileItem,
|
||||
): Promise<{ width: number; height: number }> {
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
};
|
||||
img.onerror = e => {
|
||||
reject(e);
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/** 图像宽高校验 */
|
||||
// eslint-disable-next-line complexity
|
||||
export const imageSizeValidate = async (
|
||||
file: FileItem,
|
||||
rule?: ImageSizeRule,
|
||||
): Promise<string | undefined> => {
|
||||
const { maxWidth, minWidth, maxHeight, minHeight, aspectRatio } = rule || {};
|
||||
|
||||
// 未定义时不校验
|
||||
if (isNil(maxWidth || minWidth || maxHeight || minHeight || aspectRatio)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = await getImageSize(file);
|
||||
|
||||
if (maxWidth && width > maxWidth) {
|
||||
return I18n.t('imageflow_upload_error5', {
|
||||
value: `${maxWidth}px`,
|
||||
});
|
||||
}
|
||||
|
||||
if (minWidth && width < minWidth) {
|
||||
return I18n.t('imageflow_upload_error3', {
|
||||
value: `${minWidth}px`,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxHeight && height > maxHeight) {
|
||||
return I18n.t('imageflow_upload_error4', {
|
||||
value: `${maxHeight}px`,
|
||||
});
|
||||
}
|
||||
|
||||
if (minHeight && height < minHeight) {
|
||||
return I18n.t('imageflow_upload_error2', {
|
||||
value: `${minHeight}px`,
|
||||
});
|
||||
}
|
||||
if (aspectRatio && width / height > aspectRatio) {
|
||||
return I18n.t('imageflow_upload_error1');
|
||||
}
|
||||
};
|
||||
|
||||
export const acceptValidate = (fileName: string, accept?: string) => {
|
||||
if (!accept) {
|
||||
return;
|
||||
}
|
||||
const acceptList = accept.split(',');
|
||||
|
||||
const fileExtension = getFileExtension(fileName);
|
||||
const mimeType = mime.lookup(fileExtension);
|
||||
|
||||
// image/* 匹配所有的图片类型
|
||||
if (acceptList.includes('image/*') && mimeType?.startsWith?.('image/')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!acceptList.includes(`.${fileExtension}`)) {
|
||||
return I18n.t('imageflow_upload_error_type', {
|
||||
type: `${acceptList.filter(Boolean).join('/')}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validate = async (file: FileItem, rules?: UploadValidateRule) => {
|
||||
const { size, name } = file;
|
||||
|
||||
const { maxSize, imageSize, accept, customValidate } = rules || {};
|
||||
|
||||
const validators = [
|
||||
async () => await customValidate?.(file),
|
||||
() => sizeValidate(size, maxSize),
|
||||
async () => await imageSizeValidate(file, imageSize),
|
||||
() => acceptValidate(name, accept),
|
||||
];
|
||||
|
||||
for await (const validator of validators) {
|
||||
const errorMsg = await validator();
|
||||
if (errorMsg) {
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './input-form-empty.module.less';
|
||||
|
||||
export const InputFormEmpty = () => (
|
||||
<div className={styles['input-form-empty']}>
|
||||
<Typography.Text className={'text-[12px] coz-fg-dim'}>
|
||||
{I18n.t('workflow_testrun_input_form_empty')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
.input-form-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 32px;
|
||||
color: var(--coz-fg-dim);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 { useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import {
|
||||
createSchemaField,
|
||||
FormProvider,
|
||||
type ISchema,
|
||||
type JSXComponent,
|
||||
} from '@formily/react';
|
||||
import {
|
||||
createForm,
|
||||
onFormValuesChange as innerOnFormValuesChange,
|
||||
type Form,
|
||||
} from '@formily/core';
|
||||
|
||||
import {
|
||||
Input,
|
||||
FileUpload,
|
||||
Switch,
|
||||
InputInteger,
|
||||
InputNumber,
|
||||
FormItem,
|
||||
FormSection,
|
||||
VoiceSelect,
|
||||
TextArea,
|
||||
FullInput,
|
||||
InputTime,
|
||||
} from '../form-materials';
|
||||
|
||||
const SchemaField = createSchemaField({
|
||||
components: {
|
||||
Input,
|
||||
InputInteger,
|
||||
InputNumber,
|
||||
FormItem,
|
||||
FormSection,
|
||||
FileUpload,
|
||||
Switch,
|
||||
VoiceSelect,
|
||||
TextArea,
|
||||
FullInput,
|
||||
InputTime,
|
||||
},
|
||||
});
|
||||
|
||||
interface FormCoreProps {
|
||||
schema: ISchema;
|
||||
components?: Record<string, JSXComponent>;
|
||||
disabled?: boolean;
|
||||
initialValues?: any;
|
||||
onFormValuesChange?: (form: Form) => void;
|
||||
}
|
||||
|
||||
type FormCoreRef = Form<any>;
|
||||
|
||||
const FormCore = forwardRef<FormCoreRef, FormCoreProps>(
|
||||
(
|
||||
{ schema, components, initialValues, disabled, onFormValuesChange },
|
||||
ref,
|
||||
) => {
|
||||
const handleFormChange = useMemoizedFn((f: Form) => {
|
||||
onFormValuesChange?.(f);
|
||||
});
|
||||
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
initialValues,
|
||||
disabled,
|
||||
effects() {
|
||||
innerOnFormValuesChange(handleFormChange);
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => form, [form]);
|
||||
|
||||
return (
|
||||
<FormProvider form={form}>
|
||||
<SchemaField components={components} schema={schema} />
|
||||
</FormProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export type { FormCoreProps, FormCoreRef };
|
||||
|
||||
export { FormCore, SchemaField };
|
||||
|
||||
export default FormCore;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FormCore } from './form-core';
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Suspense, lazy, forwardRef } from 'react';
|
||||
|
||||
import type { FormCoreProps, FormCoreRef } from './form-core';
|
||||
|
||||
const LazyComponent = lazy(async () => await import('./form-core'));
|
||||
|
||||
export const LazyFormCore = forwardRef<FormCoreRef, FormCoreProps>(
|
||||
(props, ref) => (
|
||||
<Suspense fallback={null}>
|
||||
<LazyComponent ref={ref} {...props} />
|
||||
</Suspense>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { connect, mapProps } from '@formily/react';
|
||||
|
||||
import { FileUpload as FileUploadAdapter } from '../../file-upload';
|
||||
|
||||
export const FileUpload = connect(
|
||||
FileUploadAdapter,
|
||||
mapProps({ validateStatus: true }),
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FileUpload } from './file-upload';
|
||||
@@ -0,0 +1,49 @@
|
||||
.form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-item-label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.form-item-label-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.label-tooltip {
|
||||
color: var(--coz-fg-secondary);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.form-item-label-text {
|
||||
font-weight: 500;
|
||||
color: #1D1C23;
|
||||
}
|
||||
|
||||
.form-item-label-asterisk {
|
||||
color: var(--coz-fg-hglt-red);
|
||||
}
|
||||
|
||||
.form-item-feedback-wrap {
|
||||
min-height: 16px;
|
||||
margin-top: 4px;
|
||||
line-height: 16px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.form-item-feedback-text {
|
||||
color: var(--coz-fg-hglt-red);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* test run test form 布局的 FormItem
|
||||
*/
|
||||
import React, { type FC, type ReactNode, type PropsWithChildren } from 'react';
|
||||
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
import { isDataField } from '@formily/core';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip, Typography, Tag } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface FormItemProps {
|
||||
required?: boolean;
|
||||
label?: ReactNode;
|
||||
description?: ReactNode;
|
||||
tag?: ReactNode;
|
||||
tooltip?: ReactNode;
|
||||
feedbackText?: ReactNode;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export const FormItemAdapter: FC<PropsWithChildren<FormItemProps>> = props => {
|
||||
const {
|
||||
required,
|
||||
label,
|
||||
feedbackText,
|
||||
description,
|
||||
tooltip,
|
||||
tag,
|
||||
action,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles['form-item']}>
|
||||
<div className={styles['form-item-label']}>
|
||||
<div className={styles['form-item-label-top']}>
|
||||
<div className={styles['top-left']}>
|
||||
<span className={styles['form-item-label-text']}>{label}</span>
|
||||
{required ? (
|
||||
<span className={styles['form-item-label-asterisk']}>*</span>
|
||||
) : null}
|
||||
{tooltip ? (
|
||||
<Tooltip content={tooltip}>
|
||||
<IconCozInfoCircle className={styles['label-tooltip']} />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{tag ? (
|
||||
<Tag className={styles.tag} size="mini" color="primary">
|
||||
{tag}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
{description ? (
|
||||
<Typography.Text
|
||||
size="small"
|
||||
type="secondary"
|
||||
ellipsis={{
|
||||
showTooltip: true,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>{children}</div>
|
||||
{feedbackText ? (
|
||||
<div className={styles['form-item-feedback-wrap']}>
|
||||
<Typography.Text
|
||||
size="small"
|
||||
className={styles['form-item-feedback-text']}
|
||||
>
|
||||
{feedbackText}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FormItem = connect(
|
||||
FormItemAdapter,
|
||||
mapProps(
|
||||
{
|
||||
title: 'label',
|
||||
required: true,
|
||||
tag: true,
|
||||
description: true,
|
||||
} as any,
|
||||
(props, field) => ({
|
||||
...props,
|
||||
feedbackText:
|
||||
isDataField(field) && field.selfErrors?.length
|
||||
? field.selfErrors
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export { FormItem };
|
||||
@@ -0,0 +1,41 @@
|
||||
.form-section :global .coz-icon-button-mini {
|
||||
line-height: 0px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px 10px 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.title-collapsible {
|
||||
color: var(--coz-fg-secondary);
|
||||
font-size: 12px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.is-close {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.title-tooltip {
|
||||
font-size: 14px;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.section-context {
|
||||
padding: 0 16px 10px 16px ;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import {
|
||||
IconCozArrowDownFill,
|
||||
IconCozInfoCircle,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Collapsible, Tooltip, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './index.module.less';
|
||||
|
||||
export interface FormSectionProps {
|
||||
title?: React.ReactNode;
|
||||
tooltip?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
collapsible?: boolean;
|
||||
}
|
||||
|
||||
export const FormSection: React.FC<
|
||||
React.PropsWithChildren<FormSectionProps>
|
||||
> = ({ title, tooltip, action, collapsible, children }) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const handleExpand = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={css['form-section']}>
|
||||
<div className={css['section-header']}>
|
||||
<div
|
||||
className={css['section-title']}
|
||||
onClick={collapsible ? handleExpand : undefined}
|
||||
>
|
||||
{collapsible ? (
|
||||
<IconCozArrowDownFill
|
||||
className={cls(css['title-collapsible'], {
|
||||
[css['is-close']]: !isOpen,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Typography.Text strong>{title}</Typography.Text>
|
||||
|
||||
{tooltip ? (
|
||||
<Tooltip content={tooltip}>
|
||||
<IconCozInfoCircle className={css['title-tooltip']} />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? (
|
||||
<div
|
||||
className={css['section-action']}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{action}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Collapsible keepDOM fade isOpen={isOpen}>
|
||||
<div className={css['section-context']}>{children}</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.full-input :global {
|
||||
.editor-kit-toolbar-v2-wrapper {
|
||||
padding: 0 2px;
|
||||
|
||||
button {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
.editor-kit-container {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-input {
|
||||
min-height: 120px;
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
}
|
||||
|
||||
.modal-full-input {
|
||||
min-height: 570px;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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, useEffect, useRef, Suspense } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import cls from 'classnames';
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
import type { Editor } from '@coze-common/md-editor-adapter';
|
||||
import {
|
||||
delta2md,
|
||||
md2html,
|
||||
LazyEditorFullInputInner,
|
||||
ToolbarItemEnum,
|
||||
} from '@coze-common/md-editor-adapter';
|
||||
import { Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './full-input.module.less';
|
||||
|
||||
export interface InnerFullInputProps {
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
/** 是否可以展开,默认 true */
|
||||
expand?: boolean;
|
||||
className?: string;
|
||||
onChange: (v?: string) => void;
|
||||
onExpand?: () => void;
|
||||
}
|
||||
|
||||
export type FullInputProps = InnerFullInputProps & {
|
||||
modalTitle?: string;
|
||||
};
|
||||
|
||||
const InnerFullInputAdapter: React.FC<FullInputProps> = ({
|
||||
className,
|
||||
disabled,
|
||||
expand = true,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const editorRef = useRef<Editor | null>(null);
|
||||
const businessKeyRef = useRef(nanoid());
|
||||
const innerValueRef = useRef<string | undefined>();
|
||||
|
||||
const handleChange = (v: string) => {
|
||||
if (!editorRef.current) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* deltas => md
|
||||
*/
|
||||
const content = editorRef.current.getContent();
|
||||
const { markdown } = delta2md(content.deltas[0], content.deltas);
|
||||
/**
|
||||
* change 可能来自用户输入或者初始化,做一下 diff 来保证性能
|
||||
*/
|
||||
if (markdown !== innerValueRef.current) {
|
||||
innerValueRef.current = markdown;
|
||||
onChange(markdown);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== innerValueRef.current) {
|
||||
innerValueRef.current = value || '';
|
||||
/**
|
||||
* md => html
|
||||
*/
|
||||
editorRef.current?.setHTML(md2html(value || ''));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyEditorFullInputInner
|
||||
field="full-input"
|
||||
className={cls(css['full-input'], className)}
|
||||
businessKey={businessKeyRef.current}
|
||||
noToolbar={disabled}
|
||||
noExpand={!expand}
|
||||
getEditor={editor => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
registerToolItem={items =>
|
||||
items
|
||||
.filter(i => (i as any)?.type !== ToolbarItemEnum.Image)
|
||||
.map(i => {
|
||||
const item = i as any;
|
||||
if (item?.type && item.extraPropsToBuiltinComp) {
|
||||
item.extraPropsToBuiltinComp = {
|
||||
...item.extraPropsToBuiltinComp,
|
||||
size: 'extra-small',
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const FullInputAdapter: React.FC<FullInputProps> = ({
|
||||
expand,
|
||||
modalTitle,
|
||||
...props
|
||||
}) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
centered
|
||||
title={modalTitle}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
>
|
||||
<InnerFullInputAdapter
|
||||
expand={false}
|
||||
className={css['modal-full-input']}
|
||||
{...props}
|
||||
/>
|
||||
</Modal>
|
||||
<InnerFullInputAdapter
|
||||
expand={expand}
|
||||
onExpand={() => setModalVisible(true)}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FullInput = connect(
|
||||
FullInputAdapter,
|
||||
mapProps({ validateStatus: true }),
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FullInput } from './full-input';
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// components
|
||||
export { Input } from './input';
|
||||
export { FileUpload } from './file-upload';
|
||||
export { Switch } from './switch';
|
||||
export { InputInteger, InputNumber } from './input-number';
|
||||
export { VoiceSelect } from './voice-select';
|
||||
export { TextArea } from './text-area';
|
||||
export { FullInput } from './full-input';
|
||||
// decorators
|
||||
export { FormItem } from './form-item';
|
||||
export { FormSection } from './form-section';
|
||||
export { InputTime } from './input-time';
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 12px;
|
||||
color: rgba(var(--coze-fg-3), var(--coze-fg-3-alpha));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba(var(--coze-bg-5), var(--coze-bg-5-alpha));
|
||||
}
|
||||
&.up {
|
||||
border-top-left-radius: var(--coze-5);
|
||||
border-top-right-radius: var(--coze-5);
|
||||
}
|
||||
&.down {
|
||||
border-bottom-left-radius: var(--coze-5);
|
||||
border-bottom-right-radius: var(--coze-5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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, useMemo, useEffect } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import BigNumber, { type BigNumber as IBigNumber } from 'bignumber.js';
|
||||
import {
|
||||
IconCozArrowDown,
|
||||
IconCozArrowUp,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Input, type InputProps } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './base-input-number-v2.module.less';
|
||||
|
||||
export interface InputNumberV2Props {
|
||||
value?: string;
|
||||
style?: React.CSSProperties;
|
||||
placeholder?: string;
|
||||
validateStatus?: InputProps['validateStatus'];
|
||||
disabled?: boolean;
|
||||
'data-testid'?: string;
|
||||
onChange?: (v?: string) => void;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
/** 整型 */
|
||||
int?: boolean;
|
||||
}
|
||||
|
||||
/** 是否是合法的数字字符串 */
|
||||
function isValidNumber(str: string) {
|
||||
try {
|
||||
const value = new BigNumber(str);
|
||||
return !value.isNaN();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeNumber(str?: string) {
|
||||
if (!str || !isValidNumber(str)) {
|
||||
return;
|
||||
}
|
||||
return new BigNumber(str);
|
||||
}
|
||||
|
||||
export const InputNumberV2Adapter: React.FC<InputNumberV2Props> = ({
|
||||
int,
|
||||
onChange,
|
||||
onBlur,
|
||||
...props
|
||||
}) => {
|
||||
const isShowButtons = useMemo(() => !props.disabled, [props.disabled]);
|
||||
const verifiedRef = useRef<undefined | IBigNumber>(
|
||||
normalizeNumber(props.value),
|
||||
);
|
||||
|
||||
const fixed = (num: IBigNumber, innerInt?: boolean) =>
|
||||
innerInt ? num.toFixed(0, BigNumber.ROUND_DOWN) : num.toFixed();
|
||||
|
||||
const handleBlur = () => {
|
||||
if (props.value === '' || props.value === undefined) {
|
||||
/** 失焦时若值为空,则同时清空验证值 */
|
||||
verifiedRef.current = undefined;
|
||||
if (props.value === '') {
|
||||
// 如果是空字符串需要主动转换为 undefined
|
||||
onChange?.(undefined);
|
||||
}
|
||||
} else {
|
||||
/** 失焦时若值不为空,则需要验证值的合法性 */
|
||||
/**
|
||||
* 1. 若值本身合法,则对值做格式化
|
||||
* 2. 若值不合法,则采纳最近一次的合法值
|
||||
* 3. 若都没有,则返回 undefined
|
||||
*/
|
||||
let next: undefined | string;
|
||||
const nextBig = normalizeNumber(props.value) || verifiedRef.current;
|
||||
if (nextBig) {
|
||||
next = fixed(nextBig, int);
|
||||
}
|
||||
if (next !== props.value) {
|
||||
onChange?.(next);
|
||||
}
|
||||
}
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
const handlePlus = () => {
|
||||
let next = '1';
|
||||
if (verifiedRef.current) {
|
||||
const nextNum = verifiedRef.current.plus('1');
|
||||
next = fixed(nextNum, int);
|
||||
}
|
||||
onChange?.(next);
|
||||
};
|
||||
|
||||
const handleMinus = () => {
|
||||
let next = '0';
|
||||
if (verifiedRef.current) {
|
||||
const nextNum = verifiedRef.current.minus('1');
|
||||
next = fixed(nextNum, int);
|
||||
}
|
||||
onChange?.(next);
|
||||
};
|
||||
|
||||
/** 当值发生变化,需要把值同步到合法数字 */
|
||||
useEffect(() => {
|
||||
if (props.value === '' || props.value === undefined) {
|
||||
verifiedRef.current = undefined;
|
||||
}
|
||||
const next = normalizeNumber(props.value);
|
||||
if (next) {
|
||||
verifiedRef.current = normalizeNumber(props.value);
|
||||
}
|
||||
}, [props.value]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
onChange={onChange}
|
||||
onBlur={handleBlur}
|
||||
suffix={
|
||||
<div className={css.buttons}>
|
||||
<div className={cls(css.button, css.up)} onClick={handlePlus}>
|
||||
<IconCozArrowUp />
|
||||
</div>
|
||||
<div className={cls(css.button, css.down)} onClick={handleMinus}>
|
||||
<IconCozArrowDown />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
hideSuffix={!isShowButtons}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { CozInputNumber, type InputNumberProps } from '@coze-arch/coze-design';
|
||||
|
||||
export type BaseInputNumberAdapterProps = {
|
||||
value?: number | string;
|
||||
onChange?: (v?: number | string) => void;
|
||||
} & Pick<InputNumberProps, 'precision'>;
|
||||
|
||||
export const BaseInputNumberAdapter: React.FC<BaseInputNumberAdapterProps> = ({
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(v: number | string) => {
|
||||
onChange?.(v === '' ? undefined : v);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
return (
|
||||
<CozInputNumber
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { InputInteger } from './input-integer';
|
||||
export { InputNumber } from './input-number';
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from '@formily/react';
|
||||
|
||||
import {
|
||||
BaseInputNumberAdapter,
|
||||
type BaseInputNumberAdapterProps,
|
||||
} from './base-input-number';
|
||||
|
||||
const InputIntegerAdapter: React.FC<BaseInputNumberAdapterProps> = props => (
|
||||
<BaseInputNumberAdapter {...props} precision={0.1} />
|
||||
);
|
||||
|
||||
export const InputInteger = connect(InputIntegerAdapter);
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { connect } from '@formily/react';
|
||||
|
||||
import { BaseInputNumberAdapter } from './base-input-number';
|
||||
|
||||
export const InputNumber = connect(BaseInputNumberAdapter);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { InputTime, type InputTimeProps } from './time';
|
||||
@@ -0,0 +1,6 @@
|
||||
.input-time {
|
||||
width: 100%;
|
||||
:global .semi-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { DatePicker } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './time.module.less';
|
||||
|
||||
export interface InputTimeProps {
|
||||
className?: string;
|
||||
value?: string;
|
||||
onChange?: (v?: string) => void;
|
||||
}
|
||||
|
||||
export const InputTime: React.FC<InputTimeProps> = ({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}) => (
|
||||
<DatePicker
|
||||
className={cls(css['input-time'], className)}
|
||||
type="dateTime"
|
||||
size="small"
|
||||
showClear={false}
|
||||
showSuffix={false}
|
||||
value={value}
|
||||
onChange={(_date, dateString) => {
|
||||
if (typeof dateString === 'string' || dateString === undefined) {
|
||||
onChange?.(dateString);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Input } from './input';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
import { Input as InputCore } from '@coze-arch/coze-design';
|
||||
|
||||
const InputAdapter = props => <InputCore size="small" {...props} />;
|
||||
|
||||
export const Input = connect(InputAdapter, mapProps({ validateStatus: true }));
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Switch } from './switch';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from '@formily/react';
|
||||
import { Switch as CozSwitch } from '@coze-arch/coze-design';
|
||||
|
||||
export interface SwitchProps {
|
||||
value?: boolean;
|
||||
onChange?: (v: boolean) => void;
|
||||
}
|
||||
|
||||
const SwitchAdapter: React.FC<SwitchProps> = ({ value, ...props }) => (
|
||||
<CozSwitch checked={value} {...props} size="small" />
|
||||
);
|
||||
|
||||
export const Switch = connect(SwitchAdapter);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TextArea } from './text-area';
|
||||
@@ -0,0 +1,6 @@
|
||||
.text-area-small {
|
||||
textarea {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
@@ -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 cls from 'classnames';
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
import { TextArea as TextAreaCore } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './text-area.module.less';
|
||||
|
||||
export interface TextAreaProps {
|
||||
size?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TextAreaAdapter: React.FC<TextAreaProps> = ({
|
||||
size,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<TextAreaCore
|
||||
className={cls(
|
||||
{
|
||||
[css['text-area-small']]: size === 'small',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const TextArea = connect(
|
||||
TextAreaAdapter,
|
||||
mapProps({ validateStatus: true }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { connect, mapProps } from '@formily/react';
|
||||
import { VoiceSelect as VoiceSelectBase } from '@coze-workflow/components';
|
||||
|
||||
const VoiceSelectAdapter = props => <VoiceSelectBase {...props} />;
|
||||
|
||||
export const VoiceSelect = connect(
|
||||
VoiceSelectAdapter,
|
||||
mapProps({ validateStatus: true }),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
.form-panel-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
import styles from './form-panel-layout.module.less';
|
||||
|
||||
interface FormPanelLayoutProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FormPanelLayout: React.FC<
|
||||
React.PropsWithChildren<FormPanelLayoutProps>
|
||||
> = ({ className, children }) => (
|
||||
<div className={cls(styles['form-panel-layout'], className)}>{children}</div>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FormPanelLayout } from './form-panel-layout';
|
||||
43
frontend/packages/workflow/test-run/src/components/index.ts
Normal file
43
frontend/packages/workflow/test-run/src/components/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* base components
|
||||
*/
|
||||
export { Collapse } from './collapse';
|
||||
export { FormPanelLayout } from './form-panel';
|
||||
export { TraceIconButton, BaseTestButton } from './test-button';
|
||||
export { ResizablePanel } from './resizable-panel';
|
||||
export { BasePanel } from './resizable-panel/base-panel';
|
||||
// 禁止直接导出 form-engine 避免 formily 包被打到首屏
|
||||
// export { FormCore } from './form-engine';
|
||||
export { NodeEventInfo } from './node-event-info';
|
||||
|
||||
/**
|
||||
* feature components
|
||||
*/
|
||||
export { LogDetail } from './log-detail';
|
||||
export {
|
||||
TestsetManageProvider,
|
||||
TestsetSelect,
|
||||
TestsetEditPanel,
|
||||
type TestsetSelectProps,
|
||||
type TestsetSelectAPI,
|
||||
useTestsetManageStore,
|
||||
} from './testset';
|
||||
|
||||
export { InputFormEmpty } from './form-empty';
|
||||
export { FileIcon, FileItemStatus, isImageFile } from './file-icon';
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { type NodeResult, workflowApi } from '@coze-workflow/base/api';
|
||||
|
||||
import { typeSafeJSONParse } from '../../../utils';
|
||||
|
||||
interface Props {
|
||||
result: NodeResult;
|
||||
paging: number;
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
}
|
||||
|
||||
export default function useGetCurrentResult({
|
||||
result,
|
||||
paging,
|
||||
spaceId,
|
||||
workflowId,
|
||||
}: Props) {
|
||||
const isNodeLogNeedAsync = true;
|
||||
const { batch, isBatch } = result || {};
|
||||
|
||||
// 反序列化获取所有遍历数组
|
||||
const batchData: NodeResult[] = useMemo(() => {
|
||||
if (!isBatch) {
|
||||
return [];
|
||||
}
|
||||
const data = typeSafeJSONParse(batch);
|
||||
return (Array.isArray(data) ? data : []).map(i => {
|
||||
if (!i) {
|
||||
return i;
|
||||
}
|
||||
return {
|
||||
...i,
|
||||
/** batch 数据里面不包含该标记,手动增加 */
|
||||
isBatch: true,
|
||||
};
|
||||
});
|
||||
}, [isBatch, batch]);
|
||||
|
||||
// 当前执行日志(同步获取完整日志)
|
||||
const current: NodeResult | undefined = useMemo(() => {
|
||||
if (!isBatch) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return batchData.find(i => i?.index === paging);
|
||||
}, [paging, isBatch, batchData, result]);
|
||||
|
||||
const isUseAsyncNodeResult = () => {
|
||||
if (!isNodeLogNeedAsync) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!current.needAsync) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const { data: currentAsync } = useQuery({
|
||||
retry: 1,
|
||||
queryKey: [
|
||||
'WorkflowApiGetNodeExecuteHistory',
|
||||
workflowId,
|
||||
spaceId,
|
||||
current?.executeId,
|
||||
current?.nodeId,
|
||||
current?.NodeType,
|
||||
isBatch,
|
||||
paging,
|
||||
],
|
||||
queryFn: () =>
|
||||
workflowApi
|
||||
.GetNodeExecuteHistory({
|
||||
workflow_id: workflowId,
|
||||
space_id: spaceId,
|
||||
execute_id: current?.executeId || '',
|
||||
node_id: current?.nodeId || '',
|
||||
node_type: current?.NodeType || '',
|
||||
is_batch: isBatch,
|
||||
batch_index: isBatch ? paging : undefined,
|
||||
})
|
||||
.then(res => res.data)
|
||||
.catch(() => current),
|
||||
enabled: isUseAsyncNodeResult(),
|
||||
});
|
||||
|
||||
return {
|
||||
current: isUseAsyncNodeResult() ? currentAsync : current,
|
||||
batchData,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { LogDetail } from './log-detail';
|
||||
@@ -0,0 +1,5 @@
|
||||
.log-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 16px;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type NodeResult } from '@coze-workflow/base/api';
|
||||
|
||||
import { type WorkflowLinkLogData } from '../../types';
|
||||
import { useMarkdownModal } from '../../features/log';
|
||||
import { LogDetailPagination } from './pagination';
|
||||
import { type LogImages as LogImagesType } from './log-images';
|
||||
import { LogFields } from './log-fields';
|
||||
import useGetCurrentResult from './hooks/use-get-current-result';
|
||||
|
||||
import css from './log-detail.module.less';
|
||||
|
||||
interface LogDetailProps {
|
||||
result: NodeResult;
|
||||
node?: FlowNodeEntity;
|
||||
paginationFixedCount?: number;
|
||||
|
||||
LogImages: LogImagesType;
|
||||
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
onOpenWorkflowLink?: (data: WorkflowLinkLogData) => void;
|
||||
}
|
||||
|
||||
export const LogDetail: React.FC<LogDetailProps> = ({
|
||||
result,
|
||||
node,
|
||||
paginationFixedCount,
|
||||
|
||||
LogImages,
|
||||
|
||||
spaceId,
|
||||
workflowId,
|
||||
onOpenWorkflowLink,
|
||||
}) => {
|
||||
const { isBatch, nodeId } = result;
|
||||
/** 从 0 开始 */
|
||||
const [paging, setPaging] = useState(0);
|
||||
/** 只看错误 */
|
||||
const [onlyShowError, setOnlyShowError] = useState(false);
|
||||
|
||||
const { current, batchData } = useGetCurrentResult({
|
||||
result,
|
||||
paging,
|
||||
spaceId,
|
||||
workflowId,
|
||||
});
|
||||
|
||||
const echoCurrent = useMemo(() => {
|
||||
if (!isBatch || !onlyShowError) {
|
||||
return current;
|
||||
}
|
||||
return current?.errorInfo ? current : undefined;
|
||||
}, [isBatch, onlyShowError, current]);
|
||||
|
||||
const { modal, open } = useMarkdownModal();
|
||||
|
||||
// 当分页数据发生变化,重新选中第一项
|
||||
useLayoutEffect(() => {
|
||||
setPaging(0);
|
||||
}, [batchData]);
|
||||
|
||||
return (
|
||||
<div className={css['log-detail']}>
|
||||
{/* 分页 */}
|
||||
{isBatch ? (
|
||||
<LogDetailPagination
|
||||
paging={paging}
|
||||
data={batchData}
|
||||
fixedCount={paginationFixedCount}
|
||||
onlyShowError={onlyShowError}
|
||||
onChange={setPaging}
|
||||
onShowErrorChange={setOnlyShowError}
|
||||
/>
|
||||
) : null}
|
||||
{echoCurrent ? (
|
||||
<LogImages testRunResult={echoCurrent} nodeId={nodeId} />
|
||||
) : null}
|
||||
<LogFields
|
||||
data={echoCurrent}
|
||||
node={node}
|
||||
onPreview={open}
|
||||
onOpenWorkflowLink={onOpenWorkflowLink}
|
||||
/>
|
||||
{modal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
.log-filed-empty {
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
p {
|
||||
margin-top: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozIllusDone } from '@coze-arch/coze-design/illustrations';
|
||||
|
||||
import styles from './empty.module.less';
|
||||
|
||||
export const EmptyFiled = () => (
|
||||
<div className={styles['log-filed-empty']}>
|
||||
<IconCozIllusDone width="120" height="120" />
|
||||
<p>{I18n.t('workflow_batch_no_failed_entries')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type NodeResult } from '@coze-workflow/base/api';
|
||||
|
||||
import { type WorkflowLinkLogData } from '../../../types';
|
||||
import { generateLog } from '../../../features/log';
|
||||
import { LogField } from './log-field';
|
||||
import { EmptyFiled } from './empty';
|
||||
|
||||
interface LogFieldsProps {
|
||||
data: NodeResult | undefined;
|
||||
node?: FlowNodeEntity;
|
||||
onPreview: (value: string, path: string[]) => void;
|
||||
onOpenWorkflowLink?: (data: WorkflowLinkLogData) => void;
|
||||
}
|
||||
|
||||
export const LogFields: React.FC<LogFieldsProps> = ({
|
||||
data,
|
||||
node,
|
||||
onPreview,
|
||||
onOpenWorkflowLink,
|
||||
}) => {
|
||||
const { nodeStatus } = data || {};
|
||||
const { logs } = useMemo(() => generateLog(data, node), [data, node]);
|
||||
|
||||
if (!data) {
|
||||
return <EmptyFiled />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{logs.map((log, idx) => (
|
||||
<LogField
|
||||
key={idx}
|
||||
log={log}
|
||||
node={node}
|
||||
nodeStatus={nodeStatus}
|
||||
onPreview={onPreview}
|
||||
onOpenWorkflowLink={onOpenWorkflowLink}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type NodeExeStatus } from '@coze-arch/bot-api/workflow_api';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type WorkflowLinkLogData } from '../../../types';
|
||||
import {
|
||||
ConditionLogParser,
|
||||
OutputLogParser,
|
||||
NormalLogParser,
|
||||
isOutputLog,
|
||||
isConditionLog,
|
||||
FunctionCallLogParser,
|
||||
isFunctionCallLog,
|
||||
WorkflowLinkParser,
|
||||
isWorkflowLinkLog,
|
||||
type Log,
|
||||
} from '../../../features/log';
|
||||
|
||||
export const LogField: React.FC<{
|
||||
log: Log;
|
||||
node?: FlowNodeEntity;
|
||||
nodeStatus?: NodeExeStatus;
|
||||
onPreview: (value: string, path: string[]) => void;
|
||||
onOpenWorkflowLink?: (data: WorkflowLinkLogData) => void;
|
||||
}> = ({ log, node, nodeStatus, onPreview, onOpenWorkflowLink }) => {
|
||||
if (isConditionLog(log)) {
|
||||
return <ConditionLogParser log={log} />;
|
||||
}
|
||||
|
||||
if (isFunctionCallLog(log)) {
|
||||
return <FunctionCallLogParser log={log} />;
|
||||
}
|
||||
|
||||
if (isOutputLog(log)) {
|
||||
return (
|
||||
<OutputLogParser
|
||||
log={log}
|
||||
node={node}
|
||||
nodeStatus={nodeStatus}
|
||||
onPreview={onPreview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isWorkflowLinkLog(log)) {
|
||||
return (
|
||||
<WorkflowLinkParser log={log} onOpenWorkflowLink={onOpenWorkflowLink} />
|
||||
);
|
||||
}
|
||||
|
||||
return <NormalLogParser log={log} />;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 NodeResult } from '@coze-workflow/base';
|
||||
|
||||
/**
|
||||
* log images 业务逻辑太重了,本期暂不抽
|
||||
*/
|
||||
export type LogImages = React.FC<{
|
||||
testRunResult: NodeResult;
|
||||
nodeId?: string;
|
||||
}>;
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 批处理的分页器
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { type NodeResult } from '@coze-workflow/base/api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography, Checkbox } from '@coze-arch/coze-design';
|
||||
|
||||
import { PageSelector } from './page-selector';
|
||||
|
||||
import styles from './pagination.module.less';
|
||||
|
||||
interface LogDetailPaginationProps {
|
||||
paging: number;
|
||||
data: (NodeResult | null)[];
|
||||
onlyShowError: boolean;
|
||||
disabled?: boolean;
|
||||
fixedCount?: number;
|
||||
onChange: (page: number) => void;
|
||||
onShowErrorChange: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const LogDetailPagination: React.FC<LogDetailPaginationProps> = ({
|
||||
paging,
|
||||
data,
|
||||
onlyShowError,
|
||||
disabled,
|
||||
fixedCount,
|
||||
onChange,
|
||||
onShowErrorChange,
|
||||
}) => {
|
||||
const items = useMemo(() => {
|
||||
if (onlyShowError) {
|
||||
return data.filter(v => Boolean(v?.errorInfo));
|
||||
}
|
||||
return data;
|
||||
}, [data, onlyShowError]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (onlyShowError) {
|
||||
return `${I18n.t('workflow_batch_error_items')}: ${items.length}/${
|
||||
data.length
|
||||
}`;
|
||||
}
|
||||
return `${I18n.t('workflow_batch_total_items')}: ${data.length}`;
|
||||
}, [data, items, onlyShowError]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles['pagination-header']}>
|
||||
<Typography.Text className="font-semibold">{title}</Typography.Text>
|
||||
|
||||
<Checkbox
|
||||
checked={onlyShowError}
|
||||
disabled={disabled}
|
||||
onChange={e => {
|
||||
const checked = Boolean(e.target.checked);
|
||||
onShowErrorChange(checked);
|
||||
}}
|
||||
aria-label={I18n.t('workflow_batch_error_only')}
|
||||
>
|
||||
{I18n.t('workflow_batch_error_only')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<PageSelector
|
||||
data={items}
|
||||
paging={paging}
|
||||
onChange={onChange}
|
||||
fixedCount={fixedCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
.ui-selector {
|
||||
position: relative;
|
||||
border: 1px solid var(--Light-usage-border---color-border, rgba(29, 28, 35, 0.08));
|
||||
border-radius: 8px;
|
||||
padding: 3px 8px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.ui-selector.has-error {
|
||||
/* color: rgba(255, 68, 30, 1); */
|
||||
/* color: var(--light-usage-danger-color-danger, #F93920) */
|
||||
}
|
||||
|
||||
.ui-selector.has-value {
|
||||
background-color: var(--Light-usage-primary-light---color-primary-light-default, #F1F2FD);
|
||||
border-color: var(--Light-usage-primary---color-primary, #4D53E8);
|
||||
}
|
||||
|
||||
.ui-selector-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
color: rgba(29, 28, 35, 0.6);
|
||||
scale: 0.75;
|
||||
}
|
||||
|
||||
.ui-selector-icon.selected {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.ui-selector-placeholder {
|
||||
font-size: 14px;
|
||||
color: var(--light-usage-text-color-text-3, rgba(29, 28, 35, 0.35));
|
||||
}
|
||||
|
||||
.more-selector-content {
|
||||
width: 320px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 8px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.ui-selector-icon-common {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -8px;
|
||||
transform: scale(0.75);
|
||||
line-height: 0;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ui-selector-error-icon {
|
||||
.ui-selector-icon-common;
|
||||
color: var(--light-usage-danger-color-danger, #F93920);
|
||||
}
|
||||
|
||||
.ui-selector-warning-icon {
|
||||
.ui-selector-icon-common;
|
||||
color: var(--Light-color-orange---orange-5, #FF8500);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { type NodeResult } from '@coze-workflow/base/api';
|
||||
import {
|
||||
IconCozWarningCircleFill,
|
||||
IconCozArrowDownFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Popover, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { PageItem, checkHasError, checkHasWarning } from './page-item';
|
||||
|
||||
import s from './more-selector.module.less';
|
||||
|
||||
interface MoreSelectorProps {
|
||||
/**
|
||||
* 运行结果数组
|
||||
*/
|
||||
data: (NodeResult | null)[];
|
||||
/** 当前选择索引 */
|
||||
paging: number;
|
||||
fixedCount: number;
|
||||
|
||||
/** placeholder */
|
||||
placeholder: string;
|
||||
|
||||
/** 选择索引变更事件 */
|
||||
onChange: (p: number) => void;
|
||||
}
|
||||
|
||||
export const MoreSelector: React.FC<MoreSelectorProps> = ({
|
||||
placeholder,
|
||||
paging,
|
||||
fixedCount,
|
||||
data,
|
||||
onChange,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const selectRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentItem = useMemo(
|
||||
() => data.find(v => v?.index === paging),
|
||||
[paging, data],
|
||||
);
|
||||
|
||||
const hasError = useMemo(() => checkHasError(currentItem), [currentItem]);
|
||||
const hasWarning = useMemo(() => checkHasWarning(currentItem), [currentItem]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: number) => {
|
||||
onChange(v);
|
||||
setIsOpen(false);
|
||||
},
|
||||
[onChange, setIsOpen],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = (e: Event) => {
|
||||
if (selectRef.current?.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!popoverRef.current?.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('click', handle);
|
||||
return () => window.removeEventListener('click', handle);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
keepDOM
|
||||
trigger="custom"
|
||||
position="bottomRight"
|
||||
visible={isOpen}
|
||||
content={
|
||||
<div className={s['more-selector-content']} ref={popoverRef}>
|
||||
{data.map((v, idx) => (
|
||||
<PageItem
|
||||
data={v}
|
||||
idx={idx + fixedCount}
|
||||
paging={paging}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
getPopupContainer={() => selectRef?.current || document.body}
|
||||
>
|
||||
<div
|
||||
ref={selectRef}
|
||||
className={cls({
|
||||
[s['ui-selector']]: true,
|
||||
[s['has-value']]: !isUndefined(currentItem) || isOpen,
|
||||
[s['has-error']]: hasError,
|
||||
[s['has-warning']]: hasWarning,
|
||||
})}
|
||||
onClick={() => setIsOpen(p => !p)}
|
||||
>
|
||||
{placeholder && isUndefined(currentItem) ? (
|
||||
<div className={s['ui-selector-placeholder']}>{placeholder}</div>
|
||||
) : null}
|
||||
{!isUndefined(currentItem) && (
|
||||
<div className={s['ui-selector-content']}>
|
||||
<Typography.Text>{paging + 1}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cls({
|
||||
[s['ui-selector-icon']]: true,
|
||||
[s.selected]: isOpen,
|
||||
})}
|
||||
>
|
||||
<IconCozArrowDownFill />
|
||||
</div>
|
||||
|
||||
{hasError || hasWarning ? (
|
||||
<div
|
||||
className={cls({
|
||||
[s['ui-selector-error-icon']]: hasError,
|
||||
[s['ui-selector-warning-icon']]: hasWarning,
|
||||
})}
|
||||
>
|
||||
<IconCozWarningCircleFill />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
.page-item {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(29, 28, 35, 0.08);
|
||||
color: #1D1C23;
|
||||
font-weight: 600;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
}
|
||||
&.error .icon {
|
||||
color: #FF441E;
|
||||
}
|
||||
&.warning .icon {
|
||||
color: #FF9600;
|
||||
}
|
||||
&.active {
|
||||
border-color: #4d53e8;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
color: rgba(29, 28, 35, 0.20);
|
||||
background: rgba(46, 46, 56, 0.04);
|
||||
cursor: not-allowed;
|
||||
border-width: 0px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
|
||||
import { isNumber } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { type NodeResult } from '@coze-workflow/base/api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozWarningCircleFill } from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './page-item.module.less';
|
||||
|
||||
interface PageItemProps {
|
||||
data: NodeResult | null;
|
||||
idx: number;
|
||||
paging: number;
|
||||
onChange: (v: number) => void;
|
||||
}
|
||||
|
||||
type Nilable<T> = T | undefined | null;
|
||||
|
||||
export function checkHasError(item: Nilable<NodeResult>) {
|
||||
return Boolean(item?.errorInfo) && item?.errorLevel === 'Error';
|
||||
}
|
||||
|
||||
export function checkHasWarning(item: Nilable<NodeResult>) {
|
||||
return Boolean(item?.errorInfo) && item?.errorLevel !== 'Error';
|
||||
}
|
||||
|
||||
const PageItemEmpty: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||
<Tooltip
|
||||
content={I18n.t('workflow_detail_testrun_panel_batch_naviagte_empty')}
|
||||
>
|
||||
<div className={cls(styles['page-item'], styles['page-item-empty'])}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const PageItem: React.FC<PageItemProps> = ({
|
||||
data,
|
||||
idx,
|
||||
paging,
|
||||
onChange,
|
||||
}) => {
|
||||
const isError = useMemo(() => checkHasError(data), [data]);
|
||||
const isWarning = useMemo(() => checkHasWarning(data), [data]);
|
||||
|
||||
const page = useMemo(() => {
|
||||
const temp = data?.index;
|
||||
if (isNumber(temp)) {
|
||||
return temp;
|
||||
}
|
||||
return idx;
|
||||
}, [data, idx]);
|
||||
|
||||
const echoPage = useMemo(() => page + 1, [page]);
|
||||
|
||||
const handleChange = useCallback(() => {
|
||||
onChange(page);
|
||||
}, [page, onChange]);
|
||||
|
||||
if (!data) {
|
||||
return <PageItemEmpty key={echoPage}>{echoPage}</PageItemEmpty>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={echoPage}
|
||||
className={cls(styles['page-item'], {
|
||||
[styles.error]: isError,
|
||||
[styles.warning]: isWarning,
|
||||
[styles.active]: page === paging,
|
||||
})}
|
||||
onClick={handleChange}
|
||||
>
|
||||
{echoPage}
|
||||
{isError || isWarning ? (
|
||||
<IconCozWarningCircleFill className={styles.icon} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.page-selector {
|
||||
display: flex;
|
||||
column-gap: 12px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
@@ -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 React, { useMemo } from 'react';
|
||||
|
||||
import { type NodeResult } from '@coze-workflow/base/api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { PageItem } from './page-item';
|
||||
import { MoreSelector } from './more-selector';
|
||||
|
||||
import styles from './page-selector.module.less';
|
||||
|
||||
interface PageSelectorProps {
|
||||
paging: number;
|
||||
fixedCount?: number;
|
||||
data: (NodeResult | null)[];
|
||||
onChange: (val: number) => void;
|
||||
}
|
||||
|
||||
const MAX_FIXED_COUNT = 10;
|
||||
|
||||
export const PageSelector: React.FC<PageSelectorProps> = ({
|
||||
paging,
|
||||
fixedCount = MAX_FIXED_COUNT,
|
||||
data,
|
||||
onChange,
|
||||
}) => {
|
||||
// 固定展示的条目,最大为 10 条,不到 10 条按实际展示
|
||||
const fixedItems = useMemo(
|
||||
() => data.slice(0, fixedCount),
|
||||
[fixedCount, data],
|
||||
);
|
||||
const moreItems = useMemo(() => data.slice(fixedCount), [data]);
|
||||
|
||||
// 是否需要通过下拉框展示更多
|
||||
const hasMore = useMemo(() => data.length > fixedCount, [data, fixedCount]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex' }} className={styles['page-selector']}>
|
||||
{fixedItems.map((item, idx) => (
|
||||
<PageItem data={item} idx={idx} paging={paging} onChange={onChange} />
|
||||
))}
|
||||
|
||||
{hasMore ? (
|
||||
<MoreSelector
|
||||
paging={paging}
|
||||
fixedCount={fixedCount}
|
||||
data={moreItems}
|
||||
placeholder={I18n.t('drill_down_placeholer_select')}
|
||||
onChange={page => {
|
||||
onChange(page);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.pagination-header {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { NodeEventInfo } from './node-event-info';
|
||||
@@ -0,0 +1,5 @@
|
||||
.node-event-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { Avatar, Typography } from '@coze-arch/coze-design';
|
||||
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import styles from './node-event-info.module.less';
|
||||
|
||||
interface NodeEventInfoProps {
|
||||
event: NodeEvent | undefined;
|
||||
}
|
||||
|
||||
export const NodeEventInfo: React.FC<NodeEventInfoProps> = ({ event }) => {
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles['node-event-info']}>
|
||||
<Avatar src={event.node_icon} shape="square" size="extra-extra-small" />
|
||||
<Typography.Text>{event.node_title}</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
.base-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
border-radius: 8px;
|
||||
background-color: var(--coz-bg-plus);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
cursor: row-resize;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
column-gap: 8px;
|
||||
}
|
||||
.panel-content {
|
||||
height: 100%;
|
||||
flex-shrink: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.panel-footer {
|
||||
border-top: 1px solid var(--coz-stroke-primary);;
|
||||
}
|
||||
|
||||
.resize-bar {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
cursor: row-resize;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { isObject } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { useResize } from './use-resize';
|
||||
|
||||
import styles from './base-panel.module.less';
|
||||
|
||||
interface BasePanelProps {
|
||||
className?: string;
|
||||
/**
|
||||
* 面板头,不传不渲染
|
||||
*/
|
||||
header?: React.ReactNode;
|
||||
/**
|
||||
* 面板脚,不传不渲染
|
||||
*/
|
||||
footer?: React.ReactNode;
|
||||
/**
|
||||
* 默认初始高度,不支持响应式
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* 是否可拖拽改变高度
|
||||
*/
|
||||
resizable?:
|
||||
| boolean
|
||||
| {
|
||||
min?: number;
|
||||
max?: number;
|
||||
};
|
||||
/**
|
||||
* 点击关闭事件,仅当渲染面板头时可能触发
|
||||
*/
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const BasePanel: React.FC<React.PropsWithChildren<BasePanelProps>> = ({
|
||||
className,
|
||||
header,
|
||||
footer,
|
||||
height,
|
||||
resizable,
|
||||
onClose,
|
||||
children,
|
||||
}) => {
|
||||
const {
|
||||
height: innerHeight,
|
||||
bind,
|
||||
ref,
|
||||
dragging,
|
||||
} = useResize({
|
||||
default: height,
|
||||
...(isObject(resizable) ? resizable : {}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
styles['base-panel'],
|
||||
className,
|
||||
dragging && styles.dragging,
|
||||
)}
|
||||
style={{ height: innerHeight }}
|
||||
ref={ref}
|
||||
>
|
||||
{resizable ? (
|
||||
<div className={styles['resize-bar']} onMouseDown={bind} />
|
||||
) : null}
|
||||
{header ? (
|
||||
<div className={styles['panel-header']}>
|
||||
{header}
|
||||
<IconButton
|
||||
icon={<IconCozCross className={'text-[18px]'} />}
|
||||
color="secondary"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles['panel-content']}>{children}</div>
|
||||
{footer ? <div className={styles['panel-footer']}>{footer}</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
type MutableRefObject,
|
||||
} from 'react';
|
||||
|
||||
import { isNumber } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './resizable-panel.module.less';
|
||||
|
||||
interface ResizablePanelProps {
|
||||
className?: string;
|
||||
header?: React.ReactNode;
|
||||
headerExtra?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
hideClose?: boolean;
|
||||
onClose: () => void;
|
||||
onCloseWithoutAnimation?: () => void;
|
||||
animation?: 'slide' | 'translateY';
|
||||
translateYHeight?: string;
|
||||
innerScrollRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
export interface ResizablePanelRef {
|
||||
minimize: () => void;
|
||||
maximize: () => void;
|
||||
close: () => void;
|
||||
scrollTo: (options?: ScrollToOptions) => void;
|
||||
}
|
||||
|
||||
const MIN_HEIGHT = 156;
|
||||
|
||||
/**
|
||||
* TODO: 这里的核心伸缩能力后面想换成 semi 的 Resizable,这里先临时写一些逻辑适配
|
||||
*/
|
||||
export const ResizablePanel = forwardRef<
|
||||
ResizablePanelRef,
|
||||
React.PropsWithChildren<ResizablePanelProps>
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
hideClose,
|
||||
header,
|
||||
footer,
|
||||
onClose,
|
||||
onCloseWithoutAnimation,
|
||||
children,
|
||||
animation,
|
||||
translateYHeight = 'calc(100% - 52px)',
|
||||
innerScrollRef: innerScrollRefFromProps,
|
||||
draggable = true,
|
||||
headerExtra,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [height, setHeight] = useState<null | number>(0);
|
||||
const [isOpen, setIsOpen] = useState<null | boolean>(null);
|
||||
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const _scrollRef = useRef<HTMLDivElement>(null);
|
||||
const scrollRef = innerScrollRefFromProps || _scrollRef;
|
||||
|
||||
const isResizing = useRef(false);
|
||||
const startY = useRef(0);
|
||||
const startHeight = useRef(0);
|
||||
|
||||
const [transition, setTransition] = useState(true);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
e => {
|
||||
if (isResizing.current) {
|
||||
const newHeight = startHeight.current - (e.clientY - startY.current); // 计算新的高度
|
||||
setHeight(newHeight > MIN_HEIGHT ? newHeight : MIN_HEIGHT);
|
||||
}
|
||||
},
|
||||
[setHeight],
|
||||
);
|
||||
const handleMouseUp = useCallback(() => {
|
||||
isResizing.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove); // 取消监听
|
||||
document.removeEventListener('mouseup', handleMouseUp); // 取消监听
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
e => {
|
||||
isResizing.current = true;
|
||||
startY.current = e.clientY; // 记录鼠标开始拖拽时的 Y 轴坐标
|
||||
startHeight.current = innerRef.current?.offsetHeight || 0;
|
||||
document.addEventListener('mousemove', handleMouseMove); // 监听鼠标移动事件
|
||||
document.addEventListener('mouseup', handleMouseUp); // 监听鼠标抬起事件
|
||||
},
|
||||
[handleMouseMove, handleMouseUp],
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setTransition(true);
|
||||
onCloseWithoutAnimation?.();
|
||||
|
||||
if (animation) {
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
setHeight(0);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 250);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
minimize: () => {
|
||||
setTransition(true);
|
||||
setHeight(MIN_HEIGHT);
|
||||
setTimeout(() => setTransition(false), 250);
|
||||
},
|
||||
maximize: () => {
|
||||
setTransition(true);
|
||||
setHeight(null);
|
||||
setTimeout(() => setTransition(false), 250);
|
||||
},
|
||||
close: () => {
|
||||
handleClose();
|
||||
},
|
||||
scrollTo: (options?: ScrollToOptions) =>
|
||||
scrollRef.current?.scrollTo(options),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setHeight(null);
|
||||
setTimeout(() => {
|
||||
setTransition(false);
|
||||
}, 250);
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const styleMemo = useMemo(() => {
|
||||
if (animation === 'slide') {
|
||||
return {
|
||||
height: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
if (animation === 'translateY') {
|
||||
return {
|
||||
height: isNumber(height) ? `${height}px` : translateYHeight,
|
||||
maxHeight: translateYHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: isNumber(height) ? `${height}px` : '90%',
|
||||
};
|
||||
}, [height, animation, translateYHeight]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
style={styleMemo}
|
||||
className={cls(
|
||||
styles.container,
|
||||
{
|
||||
[styles['resizable-panel']]: !animation,
|
||||
[styles['need-transition']]: !animation && transition,
|
||||
|
||||
[styles['resizable-panel-translateY']]: animation === 'translateY',
|
||||
[styles.show]: animation === 'translateY' && isOpen,
|
||||
[styles.hide]: animation === 'translateY' && !isOpen,
|
||||
|
||||
[styles['resizable-panel-slide']]: animation === 'slide',
|
||||
[styles['slide-in']]: animation === 'slide' && isOpen,
|
||||
[styles['slide-out']]: animation === 'slide' && !isOpen,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{draggable ? (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className={styles['panel-dragging']}
|
||||
></div>
|
||||
) : null}
|
||||
|
||||
{header ? (
|
||||
<div className={styles['panel-header']}>
|
||||
{header}
|
||||
{hideClose ? null : (
|
||||
<IconButton
|
||||
icon={<IconCozCross className={'text-[18px]'} />}
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{headerExtra ? headerExtra : null}
|
||||
<div ref={scrollRef} className={styles['panel-content']}>
|
||||
{children}
|
||||
</div>
|
||||
{footer ? <div className={styles['panel-footer']}>{footer}</div> : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,154 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resizable-panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
max-height: 90%;
|
||||
|
||||
background: rgb(var(--coze-fg-white));
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 -8px 24px 0 rgba(0, 0, 0, 16%), 0 -16px 48px 0 rgba(0, 0, 0, 8%);
|
||||
z-index: 10;
|
||||
&.need-transition {
|
||||
transition: height 0.25s;
|
||||
}
|
||||
}
|
||||
|
||||
.resizable-panel-translateY {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
max-height: calc(100% - 52px);
|
||||
|
||||
background: rgb(var(--coze-fg-white));
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0px -4px 20px 0px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
|
||||
&.show {
|
||||
animation: showAnimation 0.25s cubic-bezier(0.14, 1, 0.34, 1);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
animation: hideAnimation 0.25s cubic-bezier(0.34, 1, 0.14, 1);
|
||||
}
|
||||
|
||||
@keyframes showAnimation {
|
||||
0% {
|
||||
transform: translateY(20%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hideAnimation {
|
||||
0% {
|
||||
transform: translateY(0%);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(20%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.resizable-panel-slide {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -100%; /* 初始位置在容器的右外侧 */
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: rgb(var(--coze-fg-white));
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 -8px 24px 0 rgba(0, 0, 0, 16%), 0 -16px 48px 0 rgba(0, 0, 0, 8%);
|
||||
z-index: 10;
|
||||
transition: transform 0.25s ease;
|
||||
|
||||
&.slide-in {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
&.slide-out {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.panel-dragging {
|
||||
cursor: row-resize;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: calc((100% - 100px) / 2);
|
||||
|
||||
display: block;
|
||||
|
||||
width: 100px;
|
||||
height: 4px;
|
||||
|
||||
background-color: var(--coz-stroke-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px 0 12px;
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
max-height: 56px;
|
||||
border-top: 1px solid var(--coz-stroke-primary);
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px 0 12px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
//height: calc(100% - 104px);
|
||||
flex: 1;
|
||||
//flex-shrink: 1;
|
||||
//flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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, useRef, useCallback } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
interface Config {
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目前仅支持高度可变
|
||||
*/
|
||||
export const useResize = (config: Config) => {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [height, setHeight] = useState(config.default);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
/**
|
||||
* 拖拽过程中
|
||||
*/
|
||||
const resizing = useRef(false);
|
||||
/**
|
||||
* y 轴变化
|
||||
*/
|
||||
const startY = useRef(0);
|
||||
/** 开始位置 */
|
||||
const start = useRef(0);
|
||||
|
||||
const handleMouseMove = useMemoizedFn(e => {
|
||||
if (resizing.current) {
|
||||
const newHeight = start.current - (e.clientY - startY.current); // 计算新的高度
|
||||
if (config.max && newHeight > config.max) {
|
||||
setHeight(config.max);
|
||||
} else if (config.min && newHeight < config.min) {
|
||||
setHeight(config.min);
|
||||
} else {
|
||||
setHeight(newHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
const handleMouseUp = useCallback(() => {
|
||||
resizing.current = false;
|
||||
setDragging(false);
|
||||
document.removeEventListener('mousemove', handleMouseMove); // 取消监听
|
||||
document.removeEventListener('mouseup', handleMouseUp); // 取消监听
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseDown = useMemoizedFn(e => {
|
||||
resizing.current = true;
|
||||
setDragging(true);
|
||||
startY.current = e.clientY; // 记录鼠标开始拖拽时的 Y 轴坐标
|
||||
start.current = ref.current?.offsetHeight || 0;
|
||||
document.addEventListener('mousemove', handleMouseMove); // 监听鼠标移动事件
|
||||
document.addEventListener('mouseup', handleMouseUp); // 监听鼠标抬起事件
|
||||
});
|
||||
|
||||
return {
|
||||
height,
|
||||
bind: handleMouseDown,
|
||||
ref,
|
||||
dragging,
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { IconCozPlayFill } from '@coze-arch/coze-design/icons';
|
||||
import { Button, type ButtonProps } from '@coze-arch/coze-design';
|
||||
|
||||
export const BaseTestButton: React.FC<ButtonProps> = props => (
|
||||
<Button color="green" icon={<IconCozPlayFill />} {...props} />
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { BaseTestButton } from './base-test-button';
|
||||
export { TraceIconButton } from './trace-icon-button';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozDebug } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip, type ButtonProps } from '@coze-arch/coze-design';
|
||||
|
||||
type TraceIconButtonProps = ButtonProps;
|
||||
|
||||
export const TraceIconButton: React.FC<TraceIconButtonProps> = props => (
|
||||
<Tooltip content={I18n.t('debug_btn')}>
|
||||
<IconButton
|
||||
icon={<IconCozDebug className="coz-fg-primary" />}
|
||||
color="secondary"
|
||||
{...props}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
.auto-gen {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozStopCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip, AIButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { useAutoGen } from './use-auto-gen';
|
||||
|
||||
import styles from './auto-gen-button.module.less';
|
||||
|
||||
interface AutoGenButtonProps {
|
||||
onGenerate: (data: any) => void;
|
||||
}
|
||||
|
||||
export const AutoGenButton: React.FC<AutoGenButtonProps> = ({ onGenerate }) => {
|
||||
const { generate, abort, generating } = useAutoGen();
|
||||
|
||||
const handleGenerate = useCallback(async () => {
|
||||
const data = await generate();
|
||||
if (data?.length) {
|
||||
onGenerate(data);
|
||||
}
|
||||
}, [onGenerate, generate]);
|
||||
|
||||
return (
|
||||
<div className={styles['auto-gen']}>
|
||||
{generating ? (
|
||||
<Tooltip content={I18n.t('workflow_testset_stopgen')}>
|
||||
<AIButton
|
||||
icon={<IconCozStopCircle />}
|
||||
onlyIcon={true}
|
||||
onClick={abort}
|
||||
color="aiplus"
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<AIButton
|
||||
loading={generating}
|
||||
onClick={handleGenerate}
|
||||
color="aiplus"
|
||||
size="small"
|
||||
>
|
||||
{generating
|
||||
? I18n.t('workflow_testset_generating')
|
||||
: I18n.t('workflow_testset_aigenerate')}
|
||||
</AIButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AutoGenButton } from './auto-gen-button';
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { useCallback, useRef } from 'react';
|
||||
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
import { typeSafeJSONParse } from '../../../utils';
|
||||
import { type NodeFormSchema } from '../../../types';
|
||||
|
||||
export const useAutoGen = () => {
|
||||
const { bizCtx, bizComponentSubject, generating, patch } =
|
||||
useTestsetManageStore(store => ({
|
||||
bizCtx: store.bizCtx,
|
||||
bizComponentSubject: store.bizComponentSubject,
|
||||
generating: store.generating,
|
||||
patch: store.patch,
|
||||
}));
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const generate = useCallback(async () => {
|
||||
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;
|
||||
}
|
||||
|
||||
return (typeSafeJSONParse(genCaseData[0].input) ||
|
||||
[]) as NodeFormSchema[];
|
||||
} finally {
|
||||
patch({ generating: false });
|
||||
}
|
||||
}, [bizCtx, bizComponentSubject, patch, abortRef]);
|
||||
|
||||
const abort = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
}, [abortRef]);
|
||||
|
||||
return {
|
||||
generate,
|
||||
abort,
|
||||
generating,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
.edit-form {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: rgb(var(--coze-fg-white));
|
||||
border-radius: 8px 8px 0 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.edit-form-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 12px 0 16px;
|
||||
.title-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
max-height: 56px;
|
||||
border-top: 1px solid var(--coz-stroke-primary);
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
height: calc(100% - 104px);
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
.edit-form-desc-input {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { isNil, cloneDeep } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCross, IconCozArrowLeft } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Form,
|
||||
FormTextArea,
|
||||
Spin,
|
||||
Button,
|
||||
type FormApi,
|
||||
Tooltip,
|
||||
Toast,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { IconButton, Typography } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type CaseDataDetail,
|
||||
type CaseDataBase,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
// import { AutoGenButton } from '../auto-gen-button';
|
||||
import {
|
||||
traverseTestsetNodeFormSchemas,
|
||||
getTestsetFormSubFieldName,
|
||||
transTestsetBoolSelect2Bool,
|
||||
transTestsetFormItemSchema2Form,
|
||||
getTestsetNameRules,
|
||||
} from '../../../utils';
|
||||
// import { type NodeFormSchema } from '../../../types';
|
||||
import {
|
||||
FormItemSchemaType,
|
||||
type TestsetFormValuesForBoolSelect,
|
||||
} from '../../../constants';
|
||||
import { useEditFormSchemas } from './use-edit-form-schemas';
|
||||
import { TestsetNameInput } from './name-input';
|
||||
import { EditFormSection } from './edit-form-section';
|
||||
|
||||
import styles from './chat-flow-edit-form.module.less';
|
||||
|
||||
const TESTSET_NAME_FIELD = '__TESTSET_NAME__';
|
||||
const TESTSET_DESC_FIELD = '__TESTSET_DESC__';
|
||||
|
||||
interface TestsetEditFormProps {
|
||||
data?: CaseDataDetail | null;
|
||||
onParentClose?: () => void;
|
||||
}
|
||||
|
||||
export const ChatFlowTestsetEditForm: React.FC<TestsetEditFormProps> = ({
|
||||
data,
|
||||
onParentClose,
|
||||
}) => {
|
||||
const { bizComponentSubject, bizCtx, editMode, generating, closeEditPanel } =
|
||||
useTestsetManageStore(store => ({
|
||||
bizCtx: store.bizCtx,
|
||||
bizComponentSubject: store.bizComponentSubject,
|
||||
editMode: store.editMode,
|
||||
closeEditPanel: store.closeEditPanel,
|
||||
generating: store.generating,
|
||||
}));
|
||||
|
||||
const { schemas, schemasLoading } = useEditFormSchemas(data);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const formRef = useRef<FormApi<Record<string, any>>>();
|
||||
|
||||
const title = useMemo(
|
||||
() =>
|
||||
editMode === 'edit'
|
||||
? I18n.t('workflow_testset_edit_title')
|
||||
: I18n.t('workflow_testset_create_title'),
|
||||
[editMode],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
closeEditPanel();
|
||||
}, [closeEditPanel]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setValidating(true);
|
||||
try {
|
||||
await formRef.current?.validate();
|
||||
const errors = formRef.current?.getFormState().errors;
|
||||
|
||||
if (Object.keys(errors ?? {}).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit();
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const testsetFormValues = formRef.current?.getValues();
|
||||
|
||||
if (!testsetFormValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputSchemas = cloneDeep(schemas ?? []);
|
||||
traverseTestsetNodeFormSchemas(inputSchemas, (schema, ipt) => {
|
||||
const val = testsetFormValues[getTestsetFormSubFieldName(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 = transTestsetBoolSelect2Bool(
|
||||
ipt.value as TestsetFormValuesForBoolSelect,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const caseBase: CaseDataBase = {
|
||||
name: testsetFormValues[TESTSET_NAME_FIELD],
|
||||
caseID: data?.caseBase?.caseID,
|
||||
description: testsetFormValues[TESTSET_DESC_FIELD],
|
||||
input: JSON.stringify(inputSchemas),
|
||||
};
|
||||
|
||||
await debuggerApi.SaveCaseData({
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
caseBase,
|
||||
});
|
||||
|
||||
Toast.success(I18n.t('Save_success'));
|
||||
handleClose();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleGenerate = useCallback(
|
||||
// (nextSchemas: NodeFormSchema[]) => {
|
||||
// const formValues = formRef.current?.getValues() || {};
|
||||
// const validateFields: string[] = [];
|
||||
|
||||
// traverseTestsetNodeFormSchemas(nextSchemas, (schema, ipt) => {
|
||||
// const fieldName = getTestsetFormSubFieldName(schema, ipt);
|
||||
// const value = transTestsetFormItemSchema2Form(ipt)?.value;
|
||||
// if (!isNil(value)) {
|
||||
// formValues[fieldName] = value;
|
||||
// validateFields.push(fieldName);
|
||||
// }
|
||||
// });
|
||||
|
||||
// formRef.current?.setValues(formValues);
|
||||
// // 设置值之后再校验一次
|
||||
// formRef.current?.validate(validateFields);
|
||||
// },
|
||||
// [formRef],
|
||||
// );
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.setValues({
|
||||
[TESTSET_NAME_FIELD]: data?.caseBase?.name ?? '',
|
||||
[TESTSET_DESC_FIELD]: data?.caseBase?.description,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof schemas === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const values = formRef.current?.getValues() ?? {};
|
||||
traverseTestsetNodeFormSchemas(
|
||||
schemas,
|
||||
(schema, ipt) =>
|
||||
(values[getTestsetFormSubFieldName(schema, ipt)] =
|
||||
transTestsetFormItemSchema2Form(ipt)?.value),
|
||||
);
|
||||
formRef.current?.setValues(values);
|
||||
}, [schemas]);
|
||||
|
||||
return (
|
||||
<div className={styles['edit-form']}>
|
||||
<div className={styles['panel-header']}>
|
||||
<div className={styles['header-title']}>
|
||||
<IconButton
|
||||
icon={<IconCozArrowLeft />}
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Typography.Text strong fontSize="16px">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<IconButton
|
||||
icon={<IconCozCross />}
|
||||
color="secondary"
|
||||
onClick={onParentClose}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['panel-content']}>
|
||||
<Form<Record<string, unknown>>
|
||||
showValidateIcon={false}
|
||||
getFormApi={api => (formRef.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: data?.caseBase?.name,
|
||||
isOversea: IS_OVERSEA,
|
||||
})}
|
||||
/>
|
||||
<div className={styles['edit-form-desc-input']}></div>
|
||||
|
||||
<FormTextArea
|
||||
field={TESTSET_DESC_FIELD}
|
||||
label={I18n.t('workflow_testset_desc')}
|
||||
placeholder={I18n.t('workflow_testset_desc_placeholder')}
|
||||
autosize={true}
|
||||
maxCount={200}
|
||||
maxLength={200}
|
||||
rows={2}
|
||||
/>
|
||||
<div className={styles['edit-form-title']}>
|
||||
<span className={styles['title-text']}>
|
||||
{I18n.t('workflow_testset_node_data')}
|
||||
</span>
|
||||
{/* {IS_OVERSEA || IS_BOE ? (
|
||||
<AutoGenButton onGenerate={handleGenerate} />
|
||||
) : null} */}
|
||||
</div>
|
||||
{schemasLoading ? <Spin /> : null}
|
||||
{!schemasLoading && schemas?.length
|
||||
? schemas.map(schema => (
|
||||
<EditFormSection
|
||||
key={schema.component_id}
|
||||
schema={schema}
|
||||
disabled={generating}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</Form>
|
||||
</div>
|
||||
<div className={styles['panel-footer']}>
|
||||
<Tooltip
|
||||
content={I18n.t('workflow_testset_submit_tooltip_for_expert_mode')}
|
||||
>
|
||||
<Button
|
||||
color="hgltplus"
|
||||
style={{ width: '100%' }}
|
||||
disabled={generating}
|
||||
loading={validating || submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{I18n.t('workflow_testset_edit_confirm')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
.edit-form-section {
|
||||
:global .semi-form-section-text {
|
||||
border-bottom: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 22px;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
.form-item-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
.label-text {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
&.required::after {
|
||||
content: "*";
|
||||
font-weight: 600;
|
||||
color: var(--coz-fg-hglt-red);
|
||||
}
|
||||
}
|
||||
.label-type {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* 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 { Fragment, useMemo } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
Form,
|
||||
FormSelect,
|
||||
FormTextArea,
|
||||
Avatar,
|
||||
useFieldState,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { IntelligenceType } from '@coze-arch/bot-api/intelligence_api';
|
||||
import { ComponentType } from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
import {
|
||||
getTestsetFormSubFieldType,
|
||||
getTestsetFormSubFieldName,
|
||||
getTestsetFormItemPlaceholder,
|
||||
getTestsetFormItemCustomProps,
|
||||
} from '../../../utils';
|
||||
import {
|
||||
type NodeFormSchema,
|
||||
type FormItemSchema,
|
||||
type ArrayFieldSchema,
|
||||
} from '../../../types';
|
||||
import {
|
||||
FormItemSchemaType,
|
||||
TESTSET_FORM_BOOLEAN_SELECT_OPTIONS,
|
||||
TESTSET_BOT_NAME,
|
||||
} from '../../../constants';
|
||||
|
||||
import styles from './edit-form-section.module.less';
|
||||
|
||||
/** 整数类型表单精度 */
|
||||
const INTEGER_PRECISION = 0.1;
|
||||
|
||||
/** bot field name 固定值 */
|
||||
const BOT_FIELD_NAME = `${TESTSET_BOT_NAME}_${ComponentType.CozeVariableBot}`;
|
||||
|
||||
interface EditFormSectionTitleProps {
|
||||
schema: NodeFormSchema;
|
||||
}
|
||||
|
||||
const EditFormSectionTitle: React.FC<EditFormSectionTitleProps> = ({
|
||||
schema,
|
||||
}) => {
|
||||
const title = useMemo(() => {
|
||||
// 目前只有start和variable两种节点
|
||||
switch (schema.component_type) {
|
||||
case ComponentType.CozeStartNode:
|
||||
return I18n.t('workflow_testset_start_node');
|
||||
case ComponentType.CozeVariableBot:
|
||||
return I18n.t('workflow_testset_vardatabase_node');
|
||||
case ComponentType.CozeVariableChat:
|
||||
return I18n.t('wf_chatflow_72');
|
||||
default:
|
||||
return schema.component_name;
|
||||
}
|
||||
}, [schema]);
|
||||
|
||||
return (
|
||||
<div className={styles['form-section-title']}>
|
||||
{schema.component_icon ? (
|
||||
<Avatar src={schema.component_icon} shape="square" size="extra-small" />
|
||||
) : null}
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormLabelProps {
|
||||
schema: FormItemSchema;
|
||||
}
|
||||
|
||||
const FormLabel: React.FC<FormLabelProps> = ({ schema }) => {
|
||||
const label = useMemo(() => {
|
||||
if (schema.type === FormItemSchemaType.BOT) {
|
||||
return I18n.t('workflow_testset_vardatabase_tip');
|
||||
} else if (schema.type === FormItemSchemaType.CHAT) {
|
||||
return I18n.t('wf_chatflow_74');
|
||||
}
|
||||
return schema.name;
|
||||
}, [schema]);
|
||||
const typeLabel = useMemo(() => {
|
||||
switch (schema.type) {
|
||||
case FormItemSchemaType.STRING:
|
||||
case FormItemSchemaType.FLOAT:
|
||||
case FormItemSchemaType.NUMBER:
|
||||
case FormItemSchemaType.OBJECT:
|
||||
case FormItemSchemaType.BOOLEAN:
|
||||
case FormItemSchemaType.INTEGER:
|
||||
case FormItemSchemaType.TIME:
|
||||
return getTestsetFormSubFieldType(schema.type);
|
||||
case FormItemSchemaType.LIST: {
|
||||
const subType = (schema.schema as ArrayFieldSchema).type;
|
||||
return subType
|
||||
? `Array<${getTestsetFormSubFieldType(subType)}>`
|
||||
: 'Array';
|
||||
}
|
||||
case FormItemSchemaType.BOT:
|
||||
return '';
|
||||
case FormItemSchemaType.CHAT:
|
||||
return '';
|
||||
default:
|
||||
return schema.type;
|
||||
}
|
||||
}, [schema]);
|
||||
|
||||
return (
|
||||
<div className={styles['form-item-label']}>
|
||||
<div
|
||||
className={cls(styles['label-text'], {
|
||||
[styles.required]: schema.required,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{typeLabel ? (
|
||||
<div className={styles['label-type']}>{typeLabel}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormItemProps {
|
||||
schema: FormItemSchema;
|
||||
formSchema: NodeFormSchema;
|
||||
disabled?: boolean;
|
||||
projectId?: string;
|
||||
}
|
||||
const FormItem: React.FC<FormItemProps> = ({
|
||||
schema,
|
||||
formSchema,
|
||||
disabled,
|
||||
projectId,
|
||||
}) => {
|
||||
const { type, name, required } = schema;
|
||||
const { formRenders } = useTestsetManageStore(store => ({
|
||||
formRenders: store.formRenders,
|
||||
}));
|
||||
const CustomFormItem = formRenders?.[type];
|
||||
const fieldName = getTestsetFormSubFieldName(formSchema, schema);
|
||||
const placeholder = getTestsetFormItemPlaceholder(schema);
|
||||
const requiredMsg = I18n.t('workflow_testset_required_tip', {
|
||||
param_name:
|
||||
schema.type === FormItemSchemaType.BOT ||
|
||||
schema.type === FormItemSchemaType.CHAT
|
||||
? ''
|
||||
: name,
|
||||
});
|
||||
|
||||
if (typeof CustomFormItem !== 'undefined') {
|
||||
return (
|
||||
<CustomFormItem
|
||||
field={fieldName}
|
||||
disabled={disabled}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
{...getTestsetFormItemCustomProps(schema, projectId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FormItemSchemaType.BOOLEAN:
|
||||
return (
|
||||
<FormSelect
|
||||
className={styles['form-item-select']}
|
||||
field={fieldName}
|
||||
disabled={disabled}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
optionList={TESTSET_FORM_BOOLEAN_SELECT_OPTIONS}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
showClear={!required}
|
||||
/>
|
||||
);
|
||||
case FormItemSchemaType.INTEGER:
|
||||
case FormItemSchemaType.FLOAT:
|
||||
case FormItemSchemaType.NUMBER:
|
||||
return (
|
||||
<Form.InputNumber
|
||||
field={fieldName}
|
||||
trigger={['change', 'blur']}
|
||||
precision={
|
||||
type === FormItemSchemaType.INTEGER ? INTEGER_PRECISION : undefined
|
||||
}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
disabled={disabled}
|
||||
noLabel={true}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
case FormItemSchemaType.OBJECT:
|
||||
case FormItemSchemaType.LIST:
|
||||
return (
|
||||
<FormTextArea
|
||||
field={fieldName}
|
||||
trigger={['change', 'blur']}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
disabled={disabled}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
case FormItemSchemaType.TIME:
|
||||
return <Form.DatePicker type="dateTime" field={fieldName} />;
|
||||
case FormItemSchemaType.STRING:
|
||||
default:
|
||||
return (
|
||||
<FormTextArea
|
||||
field={fieldName}
|
||||
autosize={{ minRows: 2, maxRows: 5 }}
|
||||
trigger={['change', 'blur']}
|
||||
rules={[{ required, message: requiredMsg }]}
|
||||
disabled={disabled}
|
||||
noLabel={true}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface EditFormSectionProps {
|
||||
schema: NodeFormSchema;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const EditFormSection: React.FC<EditFormSectionProps> = ({
|
||||
schema,
|
||||
disabled,
|
||||
}) => {
|
||||
const { projectId } = useTestsetManageStore(store => ({
|
||||
projectId: store.projectId,
|
||||
}));
|
||||
|
||||
const botFieldValue = useFieldState(BOT_FIELD_NAME);
|
||||
|
||||
// 判断是否选择到了应用,只有选中应用才会回显对话组件。
|
||||
// 应用内直接默认选中应用,可以回显对话组件。
|
||||
const isBotSelectProject =
|
||||
(botFieldValue?.value?.id &&
|
||||
botFieldValue?.value?.type === IntelligenceType.Project) ||
|
||||
projectId;
|
||||
|
||||
return (
|
||||
<Form.Section
|
||||
text={<EditFormSectionTitle schema={schema} />}
|
||||
className={styles['edit-form-section']}
|
||||
>
|
||||
{schema.inputs
|
||||
.filter(i => {
|
||||
if (projectId && i.type === FormItemSchemaType.BOT) {
|
||||
return false;
|
||||
}
|
||||
// 对话组件只会在应用内存在
|
||||
if (!isBotSelectProject && i.type === FormItemSchemaType.CHAT) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((s, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<FormLabel schema={s} />
|
||||
<FormItem
|
||||
schema={s}
|
||||
formSchema={schema}
|
||||
disabled={disabled}
|
||||
projectId={botFieldValue?.value}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</Form.Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.edit-form-resizeable-panel {
|
||||
box-shadow: none;
|
||||
}
|
||||
.edit-form {
|
||||
padding: 12px;
|
||||
}
|
||||
.edit-form-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 12px 0 16px;
|
||||
.title-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.edit-form-desc-input {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { isNil, cloneDeep } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowLeft } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Form,
|
||||
FormTextArea,
|
||||
Spin,
|
||||
Button,
|
||||
type FormApi,
|
||||
Tooltip,
|
||||
Toast,
|
||||
} from '@coze-arch/coze-design';
|
||||
import {
|
||||
type CaseDataDetail,
|
||||
type CaseDataBase,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
import { AutoGenButton } from '../auto-gen-button';
|
||||
import { ResizablePanel, type ResizablePanelRef } from '../../resizable-panel';
|
||||
import {
|
||||
traverseTestsetNodeFormSchemas,
|
||||
getTestsetFormSubFieldName,
|
||||
transTestsetBoolSelect2Bool,
|
||||
transTestsetFormItemSchema2Form,
|
||||
getTestsetNameRules,
|
||||
} from '../../../utils';
|
||||
import { type NodeFormSchema } from '../../../types';
|
||||
import {
|
||||
FormItemSchemaType,
|
||||
type TestsetFormValuesForBoolSelect,
|
||||
} from '../../../constants';
|
||||
import { useEditFormSchemas } from './use-edit-form-schemas';
|
||||
import { TestsetNameInput } from './name-input';
|
||||
import { EditFormSection } from './edit-form-section';
|
||||
|
||||
import styles from './edit-form.module.less';
|
||||
|
||||
const TESTSET_NAME_FIELD = '__TESTSET_NAME__';
|
||||
const TESTSET_DESC_FIELD = '__TESTSET_DESC__';
|
||||
|
||||
interface TestsetEditFormProps {
|
||||
data?: CaseDataDetail | null;
|
||||
}
|
||||
|
||||
export const TestsetEditForm: React.FC<TestsetEditFormProps> = ({ data }) => {
|
||||
const {
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
editMode,
|
||||
generating,
|
||||
closeEditPanel,
|
||||
updateEditPanelCloseState,
|
||||
} = useTestsetManageStore(store => ({
|
||||
bizCtx: store.bizCtx,
|
||||
bizComponentSubject: store.bizComponentSubject,
|
||||
editMode: store.editMode,
|
||||
closeEditPanel: store.closeEditPanel,
|
||||
generating: store.generating,
|
||||
updateEditPanelCloseState: store.updateEditPanelCloseState,
|
||||
}));
|
||||
|
||||
const { schemas, schemasLoading } = useEditFormSchemas(data);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const formRef = useRef<FormApi<Record<string, any>>>();
|
||||
const resizablePanelRef = useRef<ResizablePanelRef | null>(null);
|
||||
|
||||
const title = useMemo(
|
||||
() => (
|
||||
<span className={'flex items-center'}>
|
||||
<IconCozArrowLeft
|
||||
className={'mr-[7px] cursor-pointer'}
|
||||
onClick={() => resizablePanelRef.current?.close?.()}
|
||||
/>
|
||||
<span className={'font-medium'}>
|
||||
{editMode === 'edit'
|
||||
? I18n.t('workflow_testset_edit_title')
|
||||
: I18n.t('workflow_testset_create_title')}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
[editMode],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
closeEditPanel();
|
||||
}, [closeEditPanel]);
|
||||
|
||||
const handleUpdateCloseState = useCallback(() => {
|
||||
updateEditPanelCloseState(false);
|
||||
}, [updateEditPanelCloseState]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setValidating(true);
|
||||
try {
|
||||
await formRef.current?.validate();
|
||||
const errors = formRef.current?.getFormState().errors;
|
||||
|
||||
if (Object.keys(errors ?? {}).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit();
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const testsetFormValues = formRef.current?.getValues();
|
||||
|
||||
if (!testsetFormValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputSchemas = cloneDeep(schemas ?? []);
|
||||
traverseTestsetNodeFormSchemas(inputSchemas, (schema, ipt) => {
|
||||
const val = testsetFormValues[getTestsetFormSubFieldName(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 = transTestsetBoolSelect2Bool(
|
||||
ipt.value as TestsetFormValuesForBoolSelect,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const caseBase: CaseDataBase = {
|
||||
name: testsetFormValues[TESTSET_NAME_FIELD],
|
||||
caseID: data?.caseBase?.caseID,
|
||||
description: testsetFormValues[TESTSET_DESC_FIELD],
|
||||
input: JSON.stringify(inputSchemas),
|
||||
};
|
||||
|
||||
await debuggerApi.SaveCaseData({
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
caseBase,
|
||||
});
|
||||
|
||||
Toast.success(I18n.t('Save_success'));
|
||||
handleClose();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = useCallback(
|
||||
(nextSchemas: NodeFormSchema[]) => {
|
||||
const formValues = formRef.current?.getValues() || {};
|
||||
const validateFields: string[] = [];
|
||||
|
||||
traverseTestsetNodeFormSchemas(nextSchemas, (schema, ipt) => {
|
||||
const fieldName = getTestsetFormSubFieldName(schema, ipt);
|
||||
const value = transTestsetFormItemSchema2Form(ipt)?.value;
|
||||
if (!isNil(value)) {
|
||||
formValues[fieldName] = value;
|
||||
validateFields.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
formRef.current?.setValues(formValues);
|
||||
// 设置值之后再校验一次
|
||||
formRef.current?.validate(validateFields);
|
||||
},
|
||||
[formRef],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.setValues({
|
||||
[TESTSET_NAME_FIELD]: data?.caseBase?.name ?? '',
|
||||
[TESTSET_DESC_FIELD]: data?.caseBase?.description,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof schemas === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const values = formRef.current?.getValues() ?? {};
|
||||
traverseTestsetNodeFormSchemas(
|
||||
schemas,
|
||||
(schema, ipt) =>
|
||||
(values[getTestsetFormSubFieldName(schema, ipt)] =
|
||||
transTestsetFormItemSchema2Form(ipt)?.value),
|
||||
);
|
||||
formRef.current?.setValues(values);
|
||||
}, [schemas]);
|
||||
|
||||
return (
|
||||
<ResizablePanel
|
||||
className={styles['edit-form-resizeable-panel']}
|
||||
draggable={false}
|
||||
animation={'slide'}
|
||||
ref={resizablePanelRef}
|
||||
header={title}
|
||||
onClose={handleClose}
|
||||
onCloseWithoutAnimation={handleUpdateCloseState}
|
||||
footer={
|
||||
<Tooltip
|
||||
content={I18n.t('workflow_testset_submit_tooltip_for_expert_mode')}
|
||||
>
|
||||
<Button
|
||||
color="hgltplus"
|
||||
style={{ width: '100%' }}
|
||||
disabled={generating}
|
||||
loading={validating || submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{I18n.t('workflow_testset_edit_confirm')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div className={styles['edit-form']}>
|
||||
<Form<Record<string, unknown>>
|
||||
showValidateIcon={false}
|
||||
getFormApi={api => (formRef.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: data?.caseBase?.name,
|
||||
isOversea: IS_OVERSEA,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className={styles['edit-form-desc-input']}></div>
|
||||
<FormTextArea
|
||||
field={TESTSET_DESC_FIELD}
|
||||
label={I18n.t('workflow_testset_desc')}
|
||||
placeholder={I18n.t('workflow_testset_desc_placeholder')}
|
||||
autosize={true}
|
||||
maxCount={200}
|
||||
maxLength={200}
|
||||
rows={2}
|
||||
/>
|
||||
<div className={styles['edit-form-title']}>
|
||||
<span className={styles['title-text']}>
|
||||
{I18n.t('workflow_testset_node_data')}
|
||||
</span>
|
||||
{IS_OVERSEA || IS_BOE ? (
|
||||
<AutoGenButton onGenerate={handleGenerate} />
|
||||
) : null}
|
||||
</div>
|
||||
{schemasLoading ? <Spin /> : null}
|
||||
{!schemasLoading && schemas?.length
|
||||
? schemas.map(schema => (
|
||||
<EditFormSection
|
||||
key={schema.component_id}
|
||||
schema={schema}
|
||||
disabled={generating}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</Form>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
import { TestsetEditForm } from './edit-form';
|
||||
import { ChatFlowTestsetEditForm } from './chat-flow-edit-form';
|
||||
|
||||
interface TestsetEditPanelProps {
|
||||
isChatFlow?: boolean;
|
||||
onParentClose?: () => void;
|
||||
}
|
||||
|
||||
export const TestsetEditPanel: React.FC<TestsetEditPanelProps> = ({
|
||||
isChatFlow,
|
||||
onParentClose,
|
||||
}) => {
|
||||
const { editPanelVisible, editData } = useTestsetManageStore(store => ({
|
||||
editPanelVisible: store.editPanelVisible,
|
||||
editData: store.editData,
|
||||
}));
|
||||
|
||||
if (!editPanelVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isChatFlow ? (
|
||||
<ChatFlowTestsetEditForm data={editData} onParentClose={onParentClose} />
|
||||
) : (
|
||||
<TestsetEditForm data={editData} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TestsetEditPanel } from './edit-panel';
|
||||
@@ -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,61 @@
|
||||
/*
|
||||
* 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 {
|
||||
Input,
|
||||
withField,
|
||||
type InputProps,
|
||||
type CommonFieldProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './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 (
|
||||
<Input
|
||||
{...props}
|
||||
maxLength={props.maxLength ?? TESTSET_NAME_MAX_LEN}
|
||||
autoComplete="off"
|
||||
onBlur={onBlur}
|
||||
suffix={
|
||||
<div className={styles.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,84 @@
|
||||
/*
|
||||
* 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 { isNil } from 'lodash-es';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type CaseDataDetail } from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
import {
|
||||
typeSafeJSONParse,
|
||||
traverseTestsetNodeFormSchemas,
|
||||
getTestsetFormSubFieldName,
|
||||
isTestsetFormSameFieldType,
|
||||
assignTestsetFormDefaultValue,
|
||||
} from '../../../utils';
|
||||
import { type NodeFormSchema, type FormItemSchema } from '../../../types';
|
||||
|
||||
export const useEditFormSchemas = (testset?: CaseDataDetail | null) => {
|
||||
const { bizCtx, bizComponentSubject } = useTestsetManageStore(store => ({
|
||||
bizCtx: store.bizCtx,
|
||||
bizComponentSubject: store.bizComponentSubject,
|
||||
}));
|
||||
const { data: schemas, loading: schemasLoading } = useRequest(
|
||||
async () => {
|
||||
const localSchemas = (typeSafeJSONParse(testset?.caseBase?.input) ||
|
||||
[]) as NodeFormSchema[];
|
||||
const res = await debuggerApi.GetSchemaByID({
|
||||
bizComponentSubject,
|
||||
bizCtx,
|
||||
});
|
||||
const remoteSchemas = (typeSafeJSONParse(res.schemaJson) ||
|
||||
[]) as NodeFormSchema[];
|
||||
|
||||
if (localSchemas.length) {
|
||||
// 编辑模式:比对本地和远程schema并尝试赋值
|
||||
const localSchemaMap: Record<string, FormItemSchema | undefined> = {};
|
||||
traverseTestsetNodeFormSchemas(
|
||||
localSchemas,
|
||||
(schema, ipt) =>
|
||||
(localSchemaMap[getTestsetFormSubFieldName(schema, ipt)] = ipt),
|
||||
);
|
||||
|
||||
traverseTestsetNodeFormSchemas(remoteSchemas, (schema, ipt) => {
|
||||
const subName = getTestsetFormSubFieldName(schema, ipt);
|
||||
const field = localSchemaMap[subName];
|
||||
|
||||
if (
|
||||
isTestsetFormSameFieldType(ipt.type, field?.type) &&
|
||||
!isNil(field?.value)
|
||||
) {
|
||||
ipt.value = field?.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 创建模式:赋默认值
|
||||
traverseTestsetNodeFormSchemas(remoteSchemas, (schema, ipt) => {
|
||||
assignTestsetFormDefaultValue(ipt);
|
||||
});
|
||||
}
|
||||
|
||||
return remoteSchemas;
|
||||
},
|
||||
{ refreshDeps: [testset] },
|
||||
);
|
||||
|
||||
return {
|
||||
schemas,
|
||||
schemasLoading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { TestsetManageProvider } from './manage-provider';
|
||||
export {
|
||||
TestsetSelect,
|
||||
type TestsetSelectProps,
|
||||
type TestsetSelectAPI,
|
||||
} from './select';
|
||||
export { TestsetEditPanel } from './edit-panel';
|
||||
export { useTestsetManageStore } from './use-testset-manage-store';
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 { create } from 'zustand';
|
||||
import {
|
||||
ComponentType,
|
||||
type BizCtx,
|
||||
type ComponentSubject,
|
||||
type CaseDataDetail,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import { validateTestsetSchema } from '../../utils';
|
||||
import type {
|
||||
ValidateSchemaResult,
|
||||
TestsetEditMode,
|
||||
NodeFormItem,
|
||||
} from '../../types';
|
||||
import { TESTSET_CONNECTOR_ID, type FormItemSchemaType } from '../../constants';
|
||||
|
||||
export interface TestsetManageState {
|
||||
bizCtx?: BizCtx;
|
||||
bizComponentSubject?: ComponentSubject;
|
||||
|
||||
projectId?: string;
|
||||
|
||||
/**
|
||||
* 校验缓存
|
||||
*/
|
||||
validateCache: ValidateSchemaResult;
|
||||
|
||||
/**
|
||||
* 编辑面板状态
|
||||
*/
|
||||
editPanelVisible: boolean;
|
||||
editData: CaseDataDetail | null;
|
||||
editMode: TestsetEditMode;
|
||||
editPanelCloseState: boolean;
|
||||
|
||||
/**
|
||||
* 自动填充状态
|
||||
*/
|
||||
generating: boolean;
|
||||
/**
|
||||
* 自定义渲染组件,暂时从外部传入,后续不要了
|
||||
*/
|
||||
formRenders: Partial<Record<FormItemSchemaType, NodeFormItem>>;
|
||||
}
|
||||
|
||||
export interface TestsetManageAction {
|
||||
/** 更新状态 */
|
||||
patch: (s: Partial<TestsetManageState>) => void;
|
||||
/**
|
||||
* 校验 schema
|
||||
*/
|
||||
validateSchema: () => Promise<ValidateSchemaResult>;
|
||||
/**
|
||||
* 打开编辑面板
|
||||
*/
|
||||
openEditPanel: (data?: CaseDataDetail) => void;
|
||||
/**
|
||||
* 关闭编辑面板
|
||||
*/
|
||||
closeEditPanel: () => void;
|
||||
|
||||
updateEditPanelCloseState: (state: boolean) => void;
|
||||
}
|
||||
|
||||
const createTestsetManageState = (initState: Partial<TestsetManageState>) => {
|
||||
const { bizCtx = {}, bizComponentSubject = {}, ...rest } = initState;
|
||||
return create<TestsetManageState & TestsetManageAction>((set, get) => ({
|
||||
bizCtx: {
|
||||
connectorID: TESTSET_CONNECTOR_ID,
|
||||
...bizCtx,
|
||||
},
|
||||
bizComponentSubject: {
|
||||
componentType: ComponentType.CozeStartNode,
|
||||
parentComponentType: ComponentType.CozeWorkflow,
|
||||
...bizComponentSubject,
|
||||
},
|
||||
|
||||
formRenders: {},
|
||||
...rest,
|
||||
validateCache: 'pending',
|
||||
editPanelVisible: false,
|
||||
editPanelCloseState: false,
|
||||
editMode: 'edit',
|
||||
editData: null,
|
||||
generating: false,
|
||||
patch: s => {
|
||||
set(() => s);
|
||||
},
|
||||
validateSchema: async () => {
|
||||
const store = get();
|
||||
if (store.validateCache !== 'pending') {
|
||||
return store.validateCache;
|
||||
}
|
||||
const res = await validateTestsetSchema({
|
||||
bizCtx: store.bizCtx,
|
||||
bizComponentSubject: store.bizComponentSubject,
|
||||
});
|
||||
set(() => ({ validateCache: res }));
|
||||
return res;
|
||||
},
|
||||
openEditPanel: data => {
|
||||
set(() => ({
|
||||
editData: data || null,
|
||||
editMode: data ? 'edit' : 'create',
|
||||
editPanelVisible: true,
|
||||
editPanelCloseState: true,
|
||||
}));
|
||||
},
|
||||
closeEditPanel: () => {
|
||||
set(() => ({
|
||||
editPanelVisible: false,
|
||||
editPanelCloseState: false,
|
||||
}));
|
||||
},
|
||||
updateEditPanelCloseState: state => {
|
||||
set(() => ({
|
||||
editPanelCloseState: state,
|
||||
}));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
type TestsetManageStore = ReturnType<typeof createTestsetManageState>;
|
||||
|
||||
export const TestsetManageContext = createContext<TestsetManageStore>(
|
||||
{} as unknown as TestsetManageStore,
|
||||
);
|
||||
|
||||
interface TestsetManageProviderProps {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
userId?: string;
|
||||
nodeId?: string;
|
||||
projectId?: string;
|
||||
formRenders?: Partial<Record<FormItemSchemaType, NodeFormItem>>;
|
||||
}
|
||||
|
||||
export const TestsetManageProvider: React.FC<
|
||||
PropsWithChildren<TestsetManageProviderProps>
|
||||
> = ({
|
||||
spaceId,
|
||||
workflowId,
|
||||
userId,
|
||||
nodeId,
|
||||
projectId,
|
||||
formRenders,
|
||||
children,
|
||||
}) => {
|
||||
// 只初始化一次
|
||||
const storeRef = useRef<TestsetManageStore>(
|
||||
createTestsetManageState({
|
||||
bizCtx: {
|
||||
bizSpaceID: spaceId,
|
||||
connectorUID: userId,
|
||||
},
|
||||
bizComponentSubject: {
|
||||
componentID: nodeId,
|
||||
parentComponentID: workflowId,
|
||||
},
|
||||
projectId,
|
||||
formRenders,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<TestsetManageContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</TestsetManageContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozPlus } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { useTestsetManageStore } from '../use-testset-manage-store';
|
||||
|
||||
interface TestsetAddButtonProps {
|
||||
onOpenEditPanel: () => void;
|
||||
}
|
||||
|
||||
export const TestsetAddButton: React.FC<TestsetAddButtonProps> = ({
|
||||
onOpenEditPanel,
|
||||
}) => {
|
||||
const { validateSchema, openEditPanel } = useTestsetManageStore(store => ({
|
||||
validateSchema: store.validateSchema,
|
||||
openEditPanel: store.openEditPanel,
|
||||
}));
|
||||
|
||||
const handleAdd = useCallback(async () => {
|
||||
const res = await validateSchema();
|
||||
if (res !== 'ok') {
|
||||
Toast.error({
|
||||
content:
|
||||
res === 'empty'
|
||||
? I18n.t('workflow_testset_peedit')
|
||||
: I18n.t('workflow_test_nodeerror'),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
openEditPanel();
|
||||
onOpenEditPanel();
|
||||
}, [onOpenEditPanel, openEditPanel, validateSchema]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<IconCozPlus />}
|
||||
color="highlight"
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{I18n.t('workflow_testset_create_btn')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
&.no-more {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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,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 { forwardRef } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './auto-load-more.module.less';
|
||||
|
||||
interface LoadMoreProps {
|
||||
noMore?: boolean;
|
||||
}
|
||||
|
||||
export const AutoLoadMore = forwardRef<HTMLDivElement, LoadMoreProps>(
|
||||
({ noMore }, ref) => (
|
||||
<div
|
||||
className={cls(styles.container, {
|
||||
[styles['no-more']]: noMore,
|
||||
})}
|
||||
ref={ref}
|
||||
>
|
||||
<Spin spinning={true} wrapperClassName={styles.spin} />
|
||||
<div className={styles.text}>{I18n.t('loading')}</div>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
TestsetSelect,
|
||||
type TestsetSelectProps,
|
||||
type TestsetSelectAPI,
|
||||
} from './select';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user