feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/* stylelint-disable block-no-empty */
|
||||
.upload-form {
|
||||
.textarea-single-line {
|
||||
:global {
|
||||
.semi-input-textarea-counter {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-multi-line {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:global {
|
||||
.semi-input-textarea-counter {}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.upload-field {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.schema_type {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-form-item {
|
||||
:global {
|
||||
.semi-form-field-label-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.add-card-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/*
|
||||
* 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 @coze-arch/max-line-per-function */
|
||||
/* eslint-disable max-lines-per-function -- refactor later */
|
||||
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { isFunction } from 'lodash-es';
|
||||
import { type WorkflowJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type FrontWorkflowInfo } from '@coze-workflow/base/types';
|
||||
import {
|
||||
type BindBizType,
|
||||
type SchemaType,
|
||||
workflowApi,
|
||||
WorkflowMode,
|
||||
} from '@coze-workflow/base/api';
|
||||
import {
|
||||
WORKFLOW_NAME_MAX_LEN,
|
||||
WORKFLOW_NAME_REGEX,
|
||||
} from '@coze-workflow/base';
|
||||
import { PictureUpload } from '@coze-common/biz-components/picture-upload';
|
||||
import { type UploadValue } from '@coze-common/biz-components';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { reporter } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useUserInfo } from '@coze-arch/foundation-sdk';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Button,
|
||||
LoadingButton,
|
||||
Space,
|
||||
Toast,
|
||||
Tooltip,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { Form, Typography, UIFormTextArea, UIModal } from '@coze-arch/bot-semi';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import s from './index.module.less';
|
||||
/** 输入合规校验异常的错误码 */
|
||||
const sensitiveWordsErrorCode = ['702095075', '702095081'];
|
||||
|
||||
const { Checkbox } = Form;
|
||||
|
||||
export interface RuleItem {
|
||||
validator: (rules: unknown[], value: string) => boolean | Error;
|
||||
}
|
||||
|
||||
interface EditWorkFlowPropsInner {
|
||||
/** 流程类型 */
|
||||
flowMode?: WorkflowMode;
|
||||
mode: 'update' | 'add';
|
||||
visible: boolean;
|
||||
// 默认confirm的disabled
|
||||
|
||||
/** 自定义弹窗标题 */
|
||||
customTitleRender?: (title: React.ReactNode) => React.ReactNode;
|
||||
|
||||
initConfirmDisabled?: boolean;
|
||||
workFlow?: FrontWorkflowInfo;
|
||||
onSuccess?: (val: {
|
||||
workflowId?: string;
|
||||
flowMode?: EditWorkFlowPropsInner['flowMode'];
|
||||
}) => void;
|
||||
onCancel?: () => void;
|
||||
/** @deprecated 未使用 */
|
||||
spaceID?: string;
|
||||
getLatestWorkflowJson?: () => Promise<WorkflowJSON>;
|
||||
bindBizId?: string;
|
||||
bindBizType?: BindBizType;
|
||||
/** 当前项目 id,只在项目内的 workflow 有该字段 */
|
||||
projectId?: string;
|
||||
nameValidators?: RuleItem[];
|
||||
}
|
||||
|
||||
/** 表单值 */
|
||||
interface FormValue {
|
||||
icon_uri: UploadValue;
|
||||
name: string;
|
||||
target: string;
|
||||
schema_type: SchemaType;
|
||||
create_conversation?: boolean;
|
||||
}
|
||||
|
||||
/** 获取弹窗标题 */
|
||||
function getModalTitle(
|
||||
mode: EditWorkFlowPropsInner['mode'],
|
||||
flowMode: EditWorkFlowPropsInner['flowMode'],
|
||||
): string {
|
||||
switch (flowMode) {
|
||||
case WorkflowMode.Imageflow:
|
||||
return mode === 'add'
|
||||
? I18n.t('imageflow_create')
|
||||
: I18n.t('imageflow_edit');
|
||||
case WorkflowMode.Workflow:
|
||||
return mode === 'add'
|
||||
? I18n.t('workflow_list_create_modal_title')
|
||||
: I18n.t('workflow_list_edit_modal_title');
|
||||
case WorkflowMode.ChatFlow:
|
||||
return mode === 'add'
|
||||
? I18n.t('wf_chatflow_81')
|
||||
: I18n.t('wf_chatflow_84');
|
||||
default:
|
||||
return mode === 'add'
|
||||
? I18n.t('workflow_list_create_modal_title')
|
||||
: I18n.t('workflow_list_edit_modal_title');
|
||||
}
|
||||
}
|
||||
|
||||
const getPictureUploadInitValue = (
|
||||
workFlow?: FrontWorkflowInfo,
|
||||
): UploadValue | undefined => {
|
||||
if (!workFlow) {
|
||||
return;
|
||||
}
|
||||
return [
|
||||
{
|
||||
url: workFlow.url || '',
|
||||
uid: workFlow.icon_uri || '',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export function CreateWorkflowModal({
|
||||
flowMode = WorkflowMode.Workflow,
|
||||
mode,
|
||||
bindBizId,
|
||||
bindBizType,
|
||||
projectId,
|
||||
visible,
|
||||
workFlow,
|
||||
initConfirmDisabled = false,
|
||||
customTitleRender,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
nameValidators = [],
|
||||
}: EditWorkFlowPropsInner) {
|
||||
const formRef = useRef<Form<Partial<FormValue>>>(null);
|
||||
const [confirmDisabled, setConfirmDisabled] = useState(initConfirmDisabled);
|
||||
const [sensitiveTip, setSensitiveTip] = useState<string | undefined>();
|
||||
const userInfo = useUserInfo();
|
||||
const currentLocale = userInfo?.locale ?? navigator.language ?? 'en-US';
|
||||
|
||||
const getValues = async () => {
|
||||
const formApi = formRef.current?.formApi;
|
||||
await formApi?.validate(['name']);
|
||||
return formApi?.getValues() as Partial<FormValue> | undefined;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
reporter.info({
|
||||
message: 'workflow_info_modal_cancel',
|
||||
namespace: 'workflow',
|
||||
});
|
||||
setSensitiveTip(undefined);
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const handleError = (error: Error & { code?: string; msg?: string }) => {
|
||||
if (sensitiveWordsErrorCode.includes(error.code || '')) {
|
||||
setConfirmDisabled(true);
|
||||
setSensitiveTip(error.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
handleCancel();
|
||||
throw error;
|
||||
};
|
||||
|
||||
const handleUpdateWorkflow = async () => {
|
||||
const workflowId = workFlow?.workflow_id;
|
||||
|
||||
reporter.info({
|
||||
message: 'workflow_info_modal_confirm_update',
|
||||
namespace: 'workflow',
|
||||
meta: {
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflowId) {
|
||||
const msg = I18n.t('workflow_list_create_modal_workflow_id_empty');
|
||||
throw new CustomError(REPORT_EVENTS.parmasValidation, msg);
|
||||
}
|
||||
const values = await getValues();
|
||||
const updateParams = {
|
||||
workflow_id: workflowId,
|
||||
icon_uri: values?.icon_uri?.[0].uid || '',
|
||||
name: values?.name,
|
||||
desc: values?.target ? values.target : '',
|
||||
space_id: workFlow.space_id || '',
|
||||
// 更新头像等信息不需要重新test run
|
||||
ignore_status_transfer: true,
|
||||
schema_type: values?.schema_type || workFlow?.schema_type,
|
||||
};
|
||||
|
||||
try {
|
||||
await workflowApi.UpdateWorkflowMeta(updateParams);
|
||||
|
||||
reporter.info({
|
||||
message: 'workflow_info_modal_update_success',
|
||||
namespace: 'workflow',
|
||||
});
|
||||
Toast.success({
|
||||
content: I18n.t('workflow_list_update_success'),
|
||||
showClose: false,
|
||||
});
|
||||
await onSuccess?.({
|
||||
workflowId,
|
||||
flowMode,
|
||||
});
|
||||
} catch (error) {
|
||||
reporter.error({
|
||||
message: 'workflow_info_modal_update_fail',
|
||||
namespace: 'workflow',
|
||||
error,
|
||||
});
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateWorkflow = async () => {
|
||||
reporter.info({
|
||||
message: 'workflow_info_modal_confirm_create',
|
||||
namespace: 'workflow',
|
||||
});
|
||||
const values = await getValues();
|
||||
try {
|
||||
const reqParams = {
|
||||
...values,
|
||||
space_id: useSpaceStore.getState().getSpaceId(),
|
||||
name: values?.name || '',
|
||||
desc: values?.target || '',
|
||||
icon_uri: values?.icon_uri?.[0]?.uid || '',
|
||||
flow_mode: flowMode,
|
||||
bind_biz_id: bindBizId,
|
||||
bind_biz_type: bindBizType,
|
||||
project_id: projectId,
|
||||
create_conversation: projectId
|
||||
? values?.create_conversation
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const resp = await workflowApi.CreateWorkflow(reqParams, {
|
||||
headers: {
|
||||
'x-locale': currentLocale,
|
||||
},
|
||||
});
|
||||
|
||||
const content =
|
||||
flowMode === WorkflowMode.Imageflow
|
||||
? I18n.t('imageflow_create_toast_success')
|
||||
: flowMode === WorkflowMode.ChatFlow
|
||||
? I18n.t('wf_chatflow_95')
|
||||
: I18n.t('workflow_list_create_success');
|
||||
Toast.success({
|
||||
content,
|
||||
showClose: false,
|
||||
});
|
||||
await onSuccess?.({
|
||||
workflowId: resp.data?.workflow_id,
|
||||
flowMode,
|
||||
});
|
||||
reporter.info({
|
||||
message: 'workflow_info_modal_create_success',
|
||||
namespace: 'workflow',
|
||||
});
|
||||
} catch (error) {
|
||||
reporter.error({
|
||||
message: 'workflow_info_modal_create_fail',
|
||||
namespace: 'workflow',
|
||||
error,
|
||||
});
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const title = useMemo(() => {
|
||||
const modelTitle = getModalTitle(mode, flowMode);
|
||||
if (customTitleRender && isFunction(customTitleRender)) {
|
||||
return customTitleRender(modelTitle);
|
||||
}
|
||||
return modelTitle;
|
||||
}, [mode, flowMode, customTitleRender]);
|
||||
const labels = useMemo<{
|
||||
nameLabel?: string;
|
||||
namePlaceholder?: string;
|
||||
descLabel?: string;
|
||||
descPlaceholder?: string;
|
||||
nameFormatRuleLabel?: string;
|
||||
nameRequiredLabel?: string;
|
||||
descRequiredLabel?: string;
|
||||
}>(() => {
|
||||
if (flowMode === WorkflowMode.Imageflow) {
|
||||
return {
|
||||
nameLabel: I18n.t('imageflow_create_name'),
|
||||
namePlaceholder: I18n.t('imageflow_create_name_placeholder'),
|
||||
descLabel: I18n.t('imageflow_create_description'),
|
||||
descPlaceholder: I18n.t('imageflow_create_description_placeholder'),
|
||||
nameRequiredLabel: I18n.t('imageflow_create_name_placeholder'),
|
||||
nameFormatRuleLabel: I18n.t('imageflow_create_name_wrong_format'),
|
||||
descRequiredLabel: I18n.t('imageflow_create_description_placeholder'),
|
||||
};
|
||||
}
|
||||
if (flowMode === WorkflowMode.ChatFlow) {
|
||||
return {
|
||||
nameLabel: I18n.t('wf_chatflow_85'),
|
||||
namePlaceholder: I18n.t('wf_chatflow_91'),
|
||||
descLabel: I18n.t('wf_chatflow_86'),
|
||||
descPlaceholder: I18n.t('wf_chatflow_92'),
|
||||
nameRequiredLabel: I18n.t('wf_chatflow_93'),
|
||||
nameFormatRuleLabel: I18n.t('wf_chatflow_94'),
|
||||
descRequiredLabel: I18n.t('wf_chatflow_122'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
nameLabel: I18n.t('workflow_list_create_modal_name_label'),
|
||||
namePlaceholder: I18n.t('workflow_list_create_modal_name_placeholder'),
|
||||
descLabel: I18n.t('workflow_list_create_modal_description_label'),
|
||||
descPlaceholder: I18n.t(
|
||||
'workflow_list_create_modal_description_placeholder',
|
||||
),
|
||||
nameRequiredLabel: I18n.t(
|
||||
'workflow_list_create_modal_name_rule_required',
|
||||
),
|
||||
nameFormatRuleLabel: I18n.t('workflow_list_create_modal_name_rule_reg'),
|
||||
descRequiredLabel: I18n.t(
|
||||
'workflow_list_create_modal_description_rule_required',
|
||||
),
|
||||
};
|
||||
}, [flowMode]);
|
||||
const iconType = useMemo(() => {
|
||||
switch (flowMode) {
|
||||
case WorkflowMode.Imageflow:
|
||||
return IconType.Imageflow;
|
||||
case WorkflowMode.Workflow:
|
||||
return IconType.Workflow;
|
||||
case WorkflowMode.ChatFlow:
|
||||
return IconType.ChatFlow;
|
||||
default:
|
||||
return IconType.Workflow;
|
||||
}
|
||||
}, [flowMode]);
|
||||
return (
|
||||
<UIModal
|
||||
type="action-small"
|
||||
keepDOM={false}
|
||||
icon={null}
|
||||
visible={visible}
|
||||
onCancel={handleCancel}
|
||||
title={title}
|
||||
footer={
|
||||
<Space>
|
||||
<Button
|
||||
className="min-w-[96px]"
|
||||
color="primary"
|
||||
onClick={handleCancel}
|
||||
data-testid="workflow.list.create.cancel"
|
||||
>
|
||||
{I18n.t('workflow_list_create_modal_footer_cancel')}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
className="min-w-[96px]"
|
||||
color="hgltplus"
|
||||
disabled={confirmDisabled}
|
||||
onClick={
|
||||
mode === 'add' ? handleCreateWorkflow : handleUpdateWorkflow
|
||||
}
|
||||
data-testid="workflow.list.create.submit"
|
||||
>
|
||||
{I18n.t('workflow_list_create_modal_footer_confirm')}
|
||||
</LoadingButton>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form<Partial<FormValue>>
|
||||
ref={formRef}
|
||||
showValidateIcon={false}
|
||||
className={s['upload-form']}
|
||||
onValueChange={({ name, target }) => {
|
||||
setSensitiveTip(undefined);
|
||||
setConfirmDisabled(!name?.trim() || !target?.trim());
|
||||
}}
|
||||
>
|
||||
<PictureUpload
|
||||
noLabel
|
||||
fieldClassName={s['upload-field']}
|
||||
field="icon_uri"
|
||||
initValue={getPictureUploadInitValue(workFlow)}
|
||||
iconType={iconType}
|
||||
fileBizType={FileBizType.BIZ_BOT_WORKFLOW}
|
||||
/>
|
||||
<UIFormTextArea
|
||||
stopValidateWithError
|
||||
className={s['textarea-single-line']}
|
||||
field="name"
|
||||
placeholder={labels.namePlaceholder}
|
||||
label={labels.nameLabel}
|
||||
// noErrorMessage
|
||||
initValue={workFlow?.name}
|
||||
rows={1}
|
||||
maxCount={WORKFLOW_NAME_MAX_LEN}
|
||||
maxLength={WORKFLOW_NAME_MAX_LEN}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: labels.nameRequiredLabel,
|
||||
},
|
||||
{
|
||||
validator(_, value) {
|
||||
if (!WORKFLOW_NAME_REGEX.test(value)) {
|
||||
return new CustomError(
|
||||
REPORT_EVENTS.formValidation,
|
||||
labels.nameFormatRuleLabel ?? '',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
...nameValidators,
|
||||
]}
|
||||
data-testid="workflow.list.create.name.input"
|
||||
/>
|
||||
|
||||
{/* 只有项目内创建 Chatflow 时才可以绑定会话 */}
|
||||
{mode === 'add' && projectId && flowMode === WorkflowMode.ChatFlow ? (
|
||||
<Checkbox
|
||||
fieldClassName={s['conversation-field']}
|
||||
noLabel
|
||||
initValue={true}
|
||||
field="create_conversation"
|
||||
>
|
||||
<Typography.Text className="coz-fg-primary">
|
||||
{I18n.t('wf_chatflow_87')}
|
||||
</Typography.Text>
|
||||
<Tooltip
|
||||
position="top"
|
||||
theme="dark"
|
||||
style={{ width: 278 }}
|
||||
content={I18n.t('wf_chatflow_82')}
|
||||
>
|
||||
<IconCozInfoCircle className="text-[16px] ml-1.5 coz-fg-dim" />
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
) : null}
|
||||
<UIFormTextArea
|
||||
field="target"
|
||||
className={s['textarea-multi-line']}
|
||||
label={labels.descLabel}
|
||||
placeholder={labels.descPlaceholder}
|
||||
initValue={workFlow?.desc}
|
||||
maxCount={600}
|
||||
maxLength={600}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: labels.descRequiredLabel,
|
||||
},
|
||||
]}
|
||||
data-testid="workflow.list.create.desc.input"
|
||||
/>
|
||||
{typeof sensitiveTip === 'string' ? (
|
||||
<Form.ErrorMessage error={sensitiveTip} />
|
||||
) : null}
|
||||
</Form>
|
||||
</UIModal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user