feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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 { InputForm } from './input-form';
|
||||
@@ -0,0 +1,30 @@
|
||||
.input-form {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: calc(100% - 48px);
|
||||
width: 100%;
|
||||
background: var(--coz-bg-max);
|
||||
}
|
||||
|
||||
.form-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
height: 32px;
|
||||
background: var(--coz-mg-hglt);
|
||||
>span {
|
||||
font-size: 14px;
|
||||
}
|
||||
// semi bug,行内元素导致高度有问题,不居中
|
||||
:global .semi-spin-wrapper{
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { SchemaForm } from '../schema-form';
|
||||
import { NodeEventInfo } from '../../../../components';
|
||||
import { useSync } from './use-sync';
|
||||
|
||||
import styles from './input-form.module.less';
|
||||
|
||||
interface QuestionFormProps {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
executeId: string;
|
||||
inputEvent?: NodeEvent;
|
||||
}
|
||||
|
||||
export const InputForm: React.FC<QuestionFormProps> = ({
|
||||
spaceId,
|
||||
workflowId,
|
||||
executeId,
|
||||
inputEvent,
|
||||
}) => {
|
||||
useSync(inputEvent);
|
||||
|
||||
if (!inputEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['input-form']}>
|
||||
<div className={styles['form-notice']}>
|
||||
<NodeEventInfo event={inputEvent} />
|
||||
<span>{I18n.t('workflow_testrun_hangup_input')}</span>
|
||||
</div>
|
||||
<div className={styles['form-content']}>
|
||||
<SchemaForm
|
||||
spaceId={spaceId}
|
||||
workflowId={workflowId}
|
||||
executeId={executeId}
|
||||
inputEvent={inputEvent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { useTestRunService } from '../../../../hooks';
|
||||
|
||||
export const useSync = (inputEvent: NodeEvent | undefined) => {
|
||||
const testRunService = useTestRunService();
|
||||
|
||||
const eventSync = useMemoizedFn((event: NodeEvent | undefined) => {
|
||||
// 结束
|
||||
if (!event) {
|
||||
testRunService.continueTestRun();
|
||||
return;
|
||||
}
|
||||
testRunService.pauseTestRun();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
eventSync(inputEvent);
|
||||
}, [inputEvent, eventSync]);
|
||||
};
|
||||
@@ -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 { SchemaForm } from './schema-form';
|
||||
@@ -0,0 +1,18 @@
|
||||
.schema-form {
|
||||
height: 100%;
|
||||
}
|
||||
.form-content {
|
||||
height: calc(100% - 56px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
.form-footer {
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--coz-stroke-primary);
|
||||
>button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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, useRef } from 'react';
|
||||
|
||||
import { type Form } from '@formily/core';
|
||||
import { workflowApi } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { translateSchema } from '../../utils';
|
||||
import { typeSafeJSONParse, stringifyValue } from '../../../../utils';
|
||||
import { useTestRunService, useFormSubmitting } from '../../../../hooks';
|
||||
import { FormCore } from '../../../../components/form-engine';
|
||||
|
||||
import styles from './schema-form.module.less';
|
||||
|
||||
interface SchemaFormProps {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
executeId: string;
|
||||
inputEvent: NodeEvent;
|
||||
}
|
||||
|
||||
export const SchemaForm: React.FC<SchemaFormProps> = ({
|
||||
spaceId,
|
||||
workflowId,
|
||||
executeId,
|
||||
inputEvent,
|
||||
}) => {
|
||||
const formRef = useRef<Form<any>>(null);
|
||||
|
||||
const submitting = useFormSubmitting(formRef.current);
|
||||
const testRunService = useTestRunService();
|
||||
|
||||
const schema = useMemo(() => {
|
||||
const data = (typeSafeJSONParse(inputEvent.data) || {}) as any;
|
||||
const temp = (typeSafeJSONParse(data.content) || []) as any[];
|
||||
return translateSchema(temp);
|
||||
}, [inputEvent]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!formRef.current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await formRef.current.submit();
|
||||
const text = JSON.stringify(stringifyValue(data));
|
||||
await workflowApi.WorkFlowTestResume({
|
||||
workflow_id: workflowId,
|
||||
space_id: spaceId,
|
||||
data: text,
|
||||
event_id: inputEvent.id || '',
|
||||
execute_id: executeId,
|
||||
});
|
||||
} finally {
|
||||
testRunService.continueTestRun();
|
||||
}
|
||||
}, [spaceId, workflowId, executeId, inputEvent, testRunService]);
|
||||
|
||||
return (
|
||||
<div className={styles['schema-form']}>
|
||||
<div className={styles['form-content']}>
|
||||
<FormCore ref={formRef} schema={schema} />
|
||||
</div>
|
||||
<div className={styles['form-footer']}>
|
||||
<Button loading={submitting} onClick={handleSubmit}>
|
||||
{I18n.t('devops_publish_multibranch_Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { ViewVariableType } from '@coze-workflow/base';
|
||||
|
||||
export const ACCEPT_MAP = {
|
||||
[ViewVariableType.Image]: ['image/*'],
|
||||
|
||||
[ViewVariableType.Doc]: ['.docx', '.doc', '.pdf'],
|
||||
|
||||
[ViewVariableType.Audio]: [
|
||||
'.mp3',
|
||||
'.wav',
|
||||
'.aac',
|
||||
'.flac',
|
||||
'.ogg',
|
||||
'.wma',
|
||||
'.alac',
|
||||
'.mid',
|
||||
'.midi',
|
||||
'.ac3',
|
||||
'.dsd',
|
||||
],
|
||||
|
||||
[ViewVariableType.Excel]: ['.xls', '.xlsx', '.csv'],
|
||||
|
||||
[ViewVariableType.Video]: ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
|
||||
|
||||
[ViewVariableType.Zip]: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
|
||||
|
||||
[ViewVariableType.Code]: ['.py', '.java', '.c', '.cpp', '.js', '.css'],
|
||||
|
||||
[ViewVariableType.Txt]: ['.txt'],
|
||||
|
||||
[ViewVariableType.Ppt]: ['.ppt', '.pptx'],
|
||||
|
||||
[ViewVariableType.Svg]: ['.svg'],
|
||||
};
|
||||
@@ -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 { InputForm } from './components/input-form';
|
||||
199
frontend/packages/workflow/test-run/src/features/input/utils.ts
Normal file
199
frontend/packages/workflow/test-run/src/features/input/utils.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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 { variableUtils } from '@coze-workflow/variable';
|
||||
import { ViewVariableType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { ACCEPT_MAP } from './constants';
|
||||
|
||||
export const getAccept = (
|
||||
inputType: ViewVariableType,
|
||||
availableFileTypes?: ViewVariableType[],
|
||||
) => {
|
||||
let accept: string;
|
||||
const itemType = ViewVariableType.isArrayType(inputType)
|
||||
? ViewVariableType.getArraySubType(inputType)
|
||||
: inputType;
|
||||
|
||||
if (itemType === ViewVariableType.File) {
|
||||
if (availableFileTypes?.length) {
|
||||
accept = availableFileTypes
|
||||
.map(type => ACCEPT_MAP[type]?.join(','))
|
||||
.join(',');
|
||||
} else {
|
||||
accept = Object.values(ACCEPT_MAP)
|
||||
.map(items => items.join(','))
|
||||
.join(',');
|
||||
}
|
||||
} else {
|
||||
accept = (ACCEPT_MAP[itemType] || []).join(',');
|
||||
}
|
||||
|
||||
return accept;
|
||||
};
|
||||
|
||||
const translateCommonField = (
|
||||
temp: any,
|
||||
viewVariableType: ViewVariableType,
|
||||
) => ({
|
||||
title: temp.name,
|
||||
type: 'string',
|
||||
'x-decorator-props': {
|
||||
tag: ViewVariableType.LabelMap[viewVariableType],
|
||||
description: temp.description,
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
required: temp.required,
|
||||
'x-validator': temp.required
|
||||
? {
|
||||
required: true,
|
||||
message: I18n.t('workflow_testset_required_tip', {
|
||||
param_name: temp.name,
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const translateFileField = (temp: any, viewVariableType: ViewVariableType) => ({
|
||||
...translateCommonField(temp, viewVariableType),
|
||||
type: 'string',
|
||||
'x-component': 'FileUpload',
|
||||
'x-component-props': {
|
||||
multiple: ViewVariableType.isArrayType(viewVariableType),
|
||||
accept: getAccept(viewVariableType),
|
||||
'data-testid': `workflow.testrun.form.component.${temp.name}`,
|
||||
fileType: [ViewVariableType.Image, ViewVariableType.ArrayImage].includes(
|
||||
viewVariableType,
|
||||
)
|
||||
? 'image'
|
||||
: 'object',
|
||||
},
|
||||
});
|
||||
|
||||
const translateVoiceField = (
|
||||
temp: any,
|
||||
viewVariableType: ViewVariableType,
|
||||
) => ({
|
||||
...translateCommonField(temp, viewVariableType),
|
||||
type: 'string',
|
||||
'x-component': 'VoiceSelect',
|
||||
'x-component-props': {
|
||||
'data-testid': `workflow.testrun.form.component.${temp.name}`,
|
||||
},
|
||||
});
|
||||
|
||||
const translateBooleanField = (
|
||||
temp: any,
|
||||
viewVariableType: ViewVariableType,
|
||||
) => ({
|
||||
...translateCommonField(temp, viewVariableType),
|
||||
type: 'boolean',
|
||||
'x-component': 'Switch',
|
||||
'x-component-props': {
|
||||
'data-testid': `workflow.testrun.form.component.${temp.name}`,
|
||||
},
|
||||
default: true,
|
||||
});
|
||||
|
||||
const translateNumberField = (
|
||||
temp: any,
|
||||
viewVariableType: ViewVariableType,
|
||||
) => ({
|
||||
...translateCommonField(temp, viewVariableType),
|
||||
type: 'number',
|
||||
'x-component':
|
||||
viewVariableType === ViewVariableType.Integer
|
||||
? 'InputInteger'
|
||||
: 'InputNumber',
|
||||
'x-component-props': {
|
||||
'data-testid': `workflow.testrun.form.component.${temp.name}`,
|
||||
},
|
||||
});
|
||||
|
||||
const translateField = (temp: any) => {
|
||||
if (!temp || !temp.type) {
|
||||
return null;
|
||||
}
|
||||
const viewVariableType = variableUtils.DTOTypeToViewType(temp.type, {
|
||||
assistType: temp.assistType || temp.schema?.assistType,
|
||||
arrayItemType: temp.schema?.type,
|
||||
});
|
||||
if (ViewVariableType.isVoiceType(viewVariableType)) {
|
||||
return translateVoiceField(temp, viewVariableType);
|
||||
}
|
||||
if (ViewVariableType.isFileType(viewVariableType)) {
|
||||
return translateFileField(temp, viewVariableType);
|
||||
}
|
||||
if (viewVariableType === ViewVariableType.Boolean) {
|
||||
return translateBooleanField(temp, viewVariableType);
|
||||
}
|
||||
if (
|
||||
viewVariableType === ViewVariableType.Number ||
|
||||
viewVariableType === ViewVariableType.Integer
|
||||
) {
|
||||
return translateNumberField(temp, viewVariableType);
|
||||
}
|
||||
if (viewVariableType === ViewVariableType.Time) {
|
||||
return translateTimeField(temp, viewVariableType);
|
||||
}
|
||||
|
||||
return {
|
||||
title: temp.name,
|
||||
// 一期固定为 string
|
||||
type: 'string',
|
||||
'x-decorator-props': {
|
||||
tag: temp.type,
|
||||
description: temp.description,
|
||||
},
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: temp.required,
|
||||
'x-component-props': {
|
||||
'data-testid': `workflow.testrun.form.component.${temp.name}`,
|
||||
},
|
||||
'x-validator': {
|
||||
required: true,
|
||||
message: I18n.t('workflow_testset_required_tip', {
|
||||
param_name: temp.name,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const translateSchema = (temp: any[]) => {
|
||||
const root = {
|
||||
type: 'object',
|
||||
properties: temp.reduce((prev, cur) => {
|
||||
const computed = translateField(cur);
|
||||
if (cur) {
|
||||
prev[cur.name] = computed;
|
||||
}
|
||||
return prev;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
const translateTimeField = (temp: any, viewVariableType: ViewVariableType) => ({
|
||||
...translateCommonField(temp, viewVariableType),
|
||||
type: 'string',
|
||||
'x-component': 'InputTime',
|
||||
'x-component-props': {
|
||||
'data-testid': `workflow.testrun.form.component.${temp.name}`,
|
||||
},
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/** 预置的特殊的 key */
|
||||
export enum LogObjSpecialKey {
|
||||
Error = '$error',
|
||||
Warning = '$warning',
|
||||
}
|
||||
|
||||
/** log 中 value 的显示样式类型 */
|
||||
export enum LogValueStyleType {
|
||||
Default,
|
||||
Number,
|
||||
Boolean,
|
||||
}
|
||||
@@ -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 { createContext } from 'react';
|
||||
|
||||
import { type DataViewerStore } from './create-store';
|
||||
|
||||
export const DataViewerContext = createContext<DataViewerStore | null>(null);
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createWithEqualityFn,
|
||||
type UseBoundStoreWithEqualityFn,
|
||||
} from 'zustand/traditional';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { type StoreApi } from 'zustand';
|
||||
|
||||
export interface DataViewerState {
|
||||
// 折叠展开的状态
|
||||
expand: Record<string, boolean> | null;
|
||||
setExpand: (key: string, v: boolean) => void;
|
||||
}
|
||||
|
||||
export type DataViewerStore = UseBoundStoreWithEqualityFn<
|
||||
StoreApi<DataViewerState>
|
||||
>;
|
||||
|
||||
export const createDataViewerStore = () =>
|
||||
createWithEqualityFn<DataViewerState>(
|
||||
set => ({
|
||||
expand: null,
|
||||
setExpand: (key: string, v: boolean) => {
|
||||
set(state => ({
|
||||
expand: {
|
||||
...state.expand,
|
||||
[key]: v,
|
||||
},
|
||||
}));
|
||||
},
|
||||
}),
|
||||
shallow,
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DataViewerProvider } from './provider';
|
||||
export { DataViewerContext } from './context';
|
||||
export { type DataViewerState } from './create-store';
|
||||
@@ -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, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { type Field } from '../types';
|
||||
import { createDataViewerStore } from './create-store';
|
||||
import { DataViewerContext } from './context';
|
||||
|
||||
interface DataViewerProviderProps {
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
export const DataViewerProvider: React.FC<
|
||||
React.PropsWithChildren<DataViewerProviderProps>
|
||||
> = ({ children, fields }) => {
|
||||
const store = useMemo(() => createDataViewerStore(), []);
|
||||
|
||||
// 根只有一项且其可以下钻时,默认展开它
|
||||
useEffect(() => {
|
||||
if (
|
||||
store.getState().expand === null &&
|
||||
fields.length === 1 &&
|
||||
fields[0]?.isObj
|
||||
) {
|
||||
store.setState({
|
||||
[fields[0].path.join('.')]: true,
|
||||
});
|
||||
}
|
||||
}, [fields, store]);
|
||||
|
||||
return (
|
||||
<DataViewerContext.Provider value={store}>
|
||||
{children}
|
||||
</DataViewerContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
.json-viewer-wrapper {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
|
||||
/** 高度限制 */
|
||||
max-height: 272px;
|
||||
min-height: 24px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 28, 35, 0.3);
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(29, 28, 35, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 React, { useMemo } from 'react';
|
||||
|
||||
import { noop } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
|
||||
import { generateFields } from './utils/generate-field';
|
||||
import type { JsonValueType } from './types';
|
||||
import { JsonField } from './json-field';
|
||||
import { DataViewerProvider } from './context';
|
||||
|
||||
import css from './data-viewer.module.less';
|
||||
|
||||
export interface DataViewerProps {
|
||||
/** 支持对象或者纯文本渲染 */
|
||||
data: JsonValueType;
|
||||
mdPreview?: boolean;
|
||||
className?: string;
|
||||
onPreview?: (value: string, path: string[]) => void;
|
||||
emptyPlaceholder?: string;
|
||||
}
|
||||
|
||||
export const DataViewer: React.FC<DataViewerProps> = ({
|
||||
data,
|
||||
mdPreview = false,
|
||||
className,
|
||||
onPreview = noop,
|
||||
emptyPlaceholder,
|
||||
}) => {
|
||||
const fields = useMemo(() => generateFields(data), [data]);
|
||||
const isTree = useMemo(() => fields.some(field => field.isObj), [fields]);
|
||||
const isEmpty = fields.length === 0;
|
||||
|
||||
if (isEmpty && emptyPlaceholder) {
|
||||
return (
|
||||
<div className="text-xs flex items-center justify-center leading-4 coz-fg-dim">
|
||||
{emptyPlaceholder}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(css['json-viewer-wrapper'], className)}
|
||||
style={isTree ? { paddingLeft: '12px' } : {}}
|
||||
draggable
|
||||
onDragStart={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DataViewerProvider fields={fields}>
|
||||
{fields.map(i => (
|
||||
<JsonField
|
||||
field={i}
|
||||
key={i.path.join('.')}
|
||||
mdPreview={mdPreview}
|
||||
onPreview={onPreview}
|
||||
/>
|
||||
))}
|
||||
</DataViewerProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { useExpand } from './use-expand';
|
||||
export { useValue } from './use-value';
|
||||
@@ -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 { useContext } from 'react';
|
||||
|
||||
import { DataViewerContext, type DataViewerState } from '../context';
|
||||
|
||||
export const useDataViewerStore = <T>(selector: (s: DataViewerState) => T) => {
|
||||
const store = useContext(DataViewerContext);
|
||||
|
||||
if (!store) {
|
||||
throw new Error('cant not found DataViewerContext');
|
||||
}
|
||||
|
||||
return store(selector);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
import { useDataViewerStore } from './use-data-viewer-store';
|
||||
|
||||
export const useExpand = (path: string) => {
|
||||
const { expand, setExpand } = useDataViewerStore(store => ({
|
||||
expand: !!store.expand?.[path],
|
||||
setExpand: store.setExpand,
|
||||
}));
|
||||
|
||||
const toggle = useMemoizedFn(() => {
|
||||
setExpand(path, !expand);
|
||||
});
|
||||
|
||||
return {
|
||||
expand,
|
||||
toggle,
|
||||
};
|
||||
};
|
||||
@@ -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 { useMemo } from 'react';
|
||||
|
||||
import { isBoolean, isNil, isNumber, isObject, isString } from 'lodash-es';
|
||||
|
||||
import { isBigNumber, bigNumberToString } from '../utils/big-number';
|
||||
import { type Field } from '../types';
|
||||
import { LogValueStyleType } from '../constants';
|
||||
|
||||
export const useValue = (value: Field['value']) => {
|
||||
const v = useMemo(() => {
|
||||
if (isNil(value)) {
|
||||
return {
|
||||
value: 'null',
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
} else if (isObject(value)) {
|
||||
// 大数字返回数字类型,值用字符串
|
||||
if (isBigNumber(value)) {
|
||||
return {
|
||||
value: bigNumberToString(value),
|
||||
type: LogValueStyleType.Number,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: '',
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
} else if (isBoolean(value)) {
|
||||
return {
|
||||
value: value.toString(),
|
||||
type: LogValueStyleType.Boolean,
|
||||
};
|
||||
} else if (isString(value)) {
|
||||
return {
|
||||
value: JSON.stringify(value),
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
} else if (isNumber(value)) {
|
||||
return {
|
||||
value,
|
||||
type: LogValueStyleType.Number,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value,
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
}, [value]);
|
||||
return v;
|
||||
};
|
||||
@@ -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 type { JsonValueType } from './types';
|
||||
|
||||
export { DataViewer, DataViewerProps } from './data-viewer';
|
||||
|
||||
export { LogObjSpecialKey, LogValueStyleType } from './constants';
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { isString, last } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowRight, IconCozEye } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { type Field } from '../types';
|
||||
import { useExpand, useValue } from '../hooks';
|
||||
import { LogObjSpecialKey, LogValueStyleType } from '../constants';
|
||||
import { isPreviewMarkdown } from '../../../utils/markdown';
|
||||
import { useTestRunReporterService } from '../../../../../hooks';
|
||||
|
||||
import styles from './json-field.module.less';
|
||||
|
||||
const SPACE_WIDTH = 14;
|
||||
|
||||
/* JSON 类型数据渲染 */
|
||||
const FieldValue: React.FC<{
|
||||
value: Field['value'];
|
||||
/** 是否是 markdown 格式 */
|
||||
isMarkdown?: boolean;
|
||||
onMarkdownPreview?: () => void;
|
||||
}> = ({ value, isMarkdown, onMarkdownPreview }) => {
|
||||
const { value: current, type } = useValue(value);
|
||||
|
||||
return (
|
||||
<span className={styles['field-value']}>
|
||||
<span
|
||||
data-testid="json-viewer-field-value"
|
||||
className={cls({
|
||||
[styles['field-value-number']]: type === LogValueStyleType.Number,
|
||||
[styles['field-value-boolean']]: type === LogValueStyleType.Boolean,
|
||||
})}
|
||||
>
|
||||
{current}
|
||||
</span>
|
||||
{/* 预览 */}
|
||||
{isMarkdown ? (
|
||||
<Button
|
||||
className={styles['value-button']}
|
||||
size="mini"
|
||||
color="primary"
|
||||
icon={<IconCozEye />}
|
||||
onClick={onMarkdownPreview}
|
||||
>
|
||||
{I18n.t('creat_project_use_template_preview')}
|
||||
</Button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const JsonField: React.FC<{
|
||||
field: Field;
|
||||
mdPreview: boolean;
|
||||
onPreview: (value: string, path: string[]) => void;
|
||||
}> = ({ field, mdPreview, onPreview }) => {
|
||||
const reporter = useTestRunReporterService();
|
||||
const { lines, children, path, isObj } = field;
|
||||
const echoLines = useMemo(() => lines.slice(1), [lines]);
|
||||
|
||||
const pathStr = useMemo(() => path.join('.'), [path]);
|
||||
|
||||
const isError = useMemo(() => pathStr === LogObjSpecialKey.Error, [pathStr]);
|
||||
const isWarning = useMemo(
|
||||
() => pathStr === LogObjSpecialKey.Warning,
|
||||
[pathStr],
|
||||
);
|
||||
|
||||
const key = useMemo(() => last(path), [path]);
|
||||
const keyWithColon = useMemo(() => {
|
||||
if (isError) {
|
||||
return I18n.t('workflow_detail_testrun_error_front');
|
||||
}
|
||||
if (isWarning) {
|
||||
return I18n.t('workflow_detail_testrun_warning_front');
|
||||
}
|
||||
return key ? `${key} : ` : '';
|
||||
}, [key, isError, isWarning]);
|
||||
|
||||
const isCanRenderMarkdown = useMemo(
|
||||
() => !isObj && !isError && !isWarning && isPreviewMarkdown(field.value),
|
||||
[isObj, isError, isWarning, field.value],
|
||||
);
|
||||
|
||||
const isRenderMarkdown = useMemo(
|
||||
() => mdPreview && isCanRenderMarkdown,
|
||||
[isCanRenderMarkdown, mdPreview],
|
||||
);
|
||||
|
||||
const { expand, toggle } = useExpand(path.join('.'));
|
||||
|
||||
const handleMarkdownPreview = () => {
|
||||
if (isString(field.value)) {
|
||||
onPreview(field.value, path);
|
||||
reporter.logOutputMarkdown({ action_type: 'preview' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isRenderMarkdown) {
|
||||
reporter.logOutputMarkdown({ action_type: 'render' });
|
||||
}
|
||||
}, [isRenderMarkdown]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles['json-viewer-field']}>
|
||||
<div
|
||||
className={styles['field-space']}
|
||||
style={{ width: `${echoLines.length * SPACE_WIDTH}px` }}
|
||||
/>
|
||||
<div
|
||||
data-testid="json-viewer-field-content"
|
||||
className={cls('field-content', styles['field-content'], {
|
||||
[styles['is-error']]: isError,
|
||||
[styles['is-warning']]: isWarning,
|
||||
})}
|
||||
onClick={isObj ? toggle : undefined}
|
||||
>
|
||||
{isObj ? (
|
||||
<>
|
||||
<span
|
||||
data-testid="json-viewer-json-field-expander"
|
||||
className={cls('field-icon', styles['field-icon'], {
|
||||
[styles.expand]: expand,
|
||||
})}
|
||||
>
|
||||
<IconCozArrowRight />
|
||||
</span>
|
||||
<span className={cls('field-key', styles['field-key'])}>
|
||||
{key}
|
||||
</span>
|
||||
<span className={cls('field-len', styles['field-len'])}>
|
||||
{` {${children.length}}`}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{keyWithColon ? (
|
||||
<span className={cls('field-key', styles['field-key'])}>
|
||||
{keyWithColon}
|
||||
</span>
|
||||
) : null}
|
||||
<FieldValue
|
||||
value={field.value}
|
||||
isMarkdown={isRenderMarkdown}
|
||||
onMarkdownPreview={handleMarkdownPreview}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{expand
|
||||
? children.map(i => (
|
||||
<JsonField
|
||||
mdPreview={mdPreview}
|
||||
onPreview={onPreview}
|
||||
field={i}
|
||||
key={i.path.join('.')}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { JsonField };
|
||||
@@ -0,0 +1,79 @@
|
||||
.json-viewer-field {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
display: flex;
|
||||
|
||||
.field-content {
|
||||
flex: 1 1 0;
|
||||
width: 0;
|
||||
padding: 2px 0;
|
||||
user-select: auto;
|
||||
|
||||
position: relative;
|
||||
|
||||
&.is-error .field-key,
|
||||
&.is-error .field-value {
|
||||
color: #FF441E;
|
||||
}
|
||||
|
||||
&.is-warning .field-key,
|
||||
&.is-warning .field-value {
|
||||
color: #FF9600;
|
||||
}
|
||||
|
||||
.field-value-number {
|
||||
color: #03B6D0;
|
||||
}
|
||||
|
||||
.field-value-boolean {
|
||||
color: #BB2BC9;
|
||||
}
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
font-size: 12px;
|
||||
color: var(--coz-fg-secondary);
|
||||
margin-right: 2px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: -12px;
|
||||
|
||||
&.expand>svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.field-block {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.field-key {
|
||||
color: var(--coz-fg-hglt);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.field-len {
|
||||
color: var(--coz-fg-dim);
|
||||
}
|
||||
}
|
||||
|
||||
.field-space {
|
||||
display: inline-block;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value-button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
* log 相关的类型
|
||||
*/
|
||||
/** 线可能存在的几种状态 */
|
||||
export enum LineStatus {
|
||||
/** 完全隐藏,最后一个父属性嵌套的子属性同列将不会有线 */
|
||||
Hidden,
|
||||
/** 完全显示,仅出现在属性相邻的线 */
|
||||
Visible,
|
||||
/** 半显示,非相邻的线 */
|
||||
Half,
|
||||
/** 最后属性的相邻线 */
|
||||
Last,
|
||||
}
|
||||
|
||||
/** JsonViewer 中的 value 可能值 */
|
||||
export type JsonValueType =
|
||||
| string
|
||||
| null
|
||||
| number
|
||||
| object
|
||||
| boolean
|
||||
| undefined;
|
||||
|
||||
export interface Field {
|
||||
/** 使用数组而不是 'a.b.c' 是因为可能存在 key='a.b' 会产生错误嵌套 */
|
||||
path: string[];
|
||||
lines: LineStatus[];
|
||||
/** 这里 value 可能是任意值,这里是不完全枚举 */
|
||||
value: JsonValueType;
|
||||
children: Field[];
|
||||
/** 是否是可下钻的对象(包含数组) */
|
||||
isObj: boolean;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
/**
|
||||
* 是不是大数字
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function isBigNumber(value: unknown): value is BigNumber {
|
||||
return !!(value && value instanceof BigNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数字转字符串
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function bigNumberToString(value: BigNumber): string {
|
||||
return value.toFixed();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 { isObject } from 'lodash-es';
|
||||
|
||||
import { LineStatus, type JsonValueType, type Field } from '../types';
|
||||
import { isBigNumber } from './big-number';
|
||||
|
||||
/**
|
||||
* 通过父元素的线条状态推到子元素的线条状态
|
||||
*/
|
||||
const getLineByParent2Child = (pLine: LineStatus): LineStatus => {
|
||||
switch (pLine) {
|
||||
/** 表示父节点也是从父父节点下钻而来,此处的子节点只需要把线延续下去即可 */
|
||||
case LineStatus.Visible:
|
||||
return LineStatus.Half;
|
||||
/** 表示父节点是父父节点的最后一个节点,子节点无需再延续,渲染空白即可 */
|
||||
case LineStatus.Last:
|
||||
return LineStatus.Hidden;
|
||||
/** 其他的情况完全继承父节点的线 */
|
||||
default:
|
||||
return pLine;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 object 解析成可以循环渲染的 fields
|
||||
* 1. 若 object 非复杂类型,则返回长度为 1 的 fields 只渲染一项
|
||||
* 2. 若 object = {},则返回长度为 0 的 fields,渲染层需要做好兜底
|
||||
*/
|
||||
const generateFields = (object: JsonValueType): Field[] => {
|
||||
/** 若 object 非复杂类型 */
|
||||
if (!isObject(object) || isBigNumber(object)) {
|
||||
return [
|
||||
{
|
||||
path: [],
|
||||
lines: [],
|
||||
value: object,
|
||||
isObj: false,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 递归计算时缓存一下计算好的线,没别的意义,降低一些时间复杂度 */
|
||||
const lineMap = new Map<string[], LineStatus[]>();
|
||||
|
||||
/** 递归解析 object 为 fields */
|
||||
const dfs = ($object: object, $parentPath: string[] = []): Field[] => {
|
||||
// 如果不是对象,直接返回空数组,兜底异常情况
|
||||
if (!isObject($object)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果是大数字,直接返回空数组
|
||||
if (isBigNumber($object)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parentLines = lineMap.get($parentPath) || [];
|
||||
|
||||
const keys = Object.keys($object);
|
||||
|
||||
return keys.map((key, idx) => {
|
||||
const value = $object[key];
|
||||
const path = $parentPath.concat(key);
|
||||
const last = idx === keys.length - 1;
|
||||
/**
|
||||
* 根据父节点的线推导子节点的线
|
||||
*/
|
||||
const lines = parentLines
|
||||
.map<LineStatus>(getLineByParent2Child)
|
||||
/**
|
||||
* 最后拼接上子节点自己的线,最后一个节点和普通的节点有样式区分
|
||||
*/
|
||||
.concat(last ? LineStatus.Last : LineStatus.Visible);
|
||||
lineMap.set(path, lines);
|
||||
return {
|
||||
path,
|
||||
lines,
|
||||
value,
|
||||
children: dfs(value, path),
|
||||
isObj: isObject(value) && !isBigNumber(value),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return dfs(object);
|
||||
};
|
||||
|
||||
export { generateFields };
|
||||
@@ -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 { generateFields } from './generate-field';
|
||||
@@ -0,0 +1,31 @@
|
||||
.preview-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
|
||||
&.only-one .image-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.columns-5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.image-item {
|
||||
max-height: 280px;
|
||||
aspect-ratio: 1 / 1; /* 宽高比为1:1 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background-color: var(--coz-bg-primary);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
object-fit: fill;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { useSize } from 'ahooks';
|
||||
import { ImagePreview, Image } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './images-preview.module.less';
|
||||
|
||||
interface ImagesPreviewProps {
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export const ImagesPreview: React.FC<ImagesPreviewProps> = ({ images }) => {
|
||||
const onlyOne = images.length === 1;
|
||||
const ref = useRef(null);
|
||||
const size = useSize(ref);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<ImagePreview
|
||||
className={cls(css['preview-group'], {
|
||||
[css['only-one']]: onlyOne,
|
||||
[css['columns-5']]: size?.width && size?.width > 420,
|
||||
})}
|
||||
getPopupContainer={() => document.body}
|
||||
>
|
||||
{images.map((url, index) => (
|
||||
<Image
|
||||
key={`${url}_${index}`}
|
||||
src={url}
|
||||
className={css['image-item']}
|
||||
/>
|
||||
))}
|
||||
</ImagePreview>
|
||||
</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 { LogImages } from './log-images';
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 { IconCozArrowBottom } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { LogWrap } from '../log-parser/log-wrap';
|
||||
import { ImagesPreview } from './images-preview';
|
||||
|
||||
interface LogImagesProps {
|
||||
images: string[];
|
||||
onDownload?: () => void;
|
||||
}
|
||||
|
||||
export const LogImages: React.FC<LogImagesProps> = ({ images, onDownload }) => {
|
||||
if (!images || !images.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LogWrap
|
||||
labelStyle={{
|
||||
height: '24px',
|
||||
}}
|
||||
label={I18n.t('imageflow_output_display')}
|
||||
copyable={false}
|
||||
extra={
|
||||
<Button
|
||||
icon={<IconCozArrowBottom />}
|
||||
color="primary"
|
||||
type="primary"
|
||||
onClick={onDownload}
|
||||
size="small"
|
||||
>
|
||||
{I18n.t('imageflow_output_display_save')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ImagesPreview images={images} />
|
||||
</LogWrap>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
.condition-field {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
column-gap: 4px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
width: 151px;
|
||||
}
|
||||
|
||||
.field-operator {
|
||||
width: 82px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.operator-value {
|
||||
width: 82px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(29, 28, 35, 0.16);
|
||||
background: #FFF;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.logic-data {
|
||||
font-size: 12px;
|
||||
margin: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { pick } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { DataViewer } from '../data-viewer';
|
||||
import {
|
||||
type ConditionData,
|
||||
type ConditionLog,
|
||||
ConditionGroup,
|
||||
} from '../../types';
|
||||
import { LogWrap } from './log-wrap';
|
||||
|
||||
import css from './condition-log-parser.module.less';
|
||||
|
||||
export const ConditionField: React.FC<{ condition: ConditionData }> = ({
|
||||
condition,
|
||||
}) => {
|
||||
const { leftData, rightData, operatorData } = condition;
|
||||
return (
|
||||
<div className={css['condition-field']}>
|
||||
<DataViewer data={leftData} className={css['field-value']} />
|
||||
<div className={css['field-operator']}>
|
||||
<Typography.Text size="small" className={css['operator-value']}>
|
||||
{operatorData}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<DataViewer data={rightData} className={css['field-value']} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConditionGroup: React.FC<{
|
||||
idx: number;
|
||||
group: ConditionGroup;
|
||||
}> = ({ idx, group }) => {
|
||||
const { name, logic, logicData, conditions } = group;
|
||||
|
||||
return (
|
||||
<LogWrap
|
||||
label={`${I18n.t('workflow_detail_condition_condition')} ${idx + 1}`}
|
||||
copyTooltip={I18n.t('workflow_detail_title_testrun_copyinput')}
|
||||
source={{
|
||||
name,
|
||||
logic,
|
||||
conditions: conditions.map(condition =>
|
||||
pick(condition, ['left', 'right', 'oprator']),
|
||||
),
|
||||
}}
|
||||
>
|
||||
{conditions.map((condition, cIdx) => (
|
||||
<>
|
||||
<ConditionField condition={condition} />
|
||||
{cIdx < conditions.length - 1 && (
|
||||
<div className={css['logic-data']}>{logicData}</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</LogWrap>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConditionLogParser: React.FC<{ log: ConditionLog }> = ({
|
||||
log,
|
||||
}) => {
|
||||
const { conditions } = log;
|
||||
|
||||
return (
|
||||
<>
|
||||
{conditions.map((group, idx) => (
|
||||
<ConditionGroup group={group} idx={idx} key={idx} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, type PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCopy } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { useCopy } from '../../../hooks/use-copy';
|
||||
|
||||
export const ContentHeader: FC<
|
||||
PropsWithChildren<{
|
||||
source: unknown;
|
||||
className?: string;
|
||||
}>
|
||||
> = ({ children, source, className }) => {
|
||||
const { handleCopy } = useCopy(source);
|
||||
|
||||
return (
|
||||
<div className={classNames('flex items-center mb-1 h-4', className)}>
|
||||
<div className="font-medium coz-fg-secondary text-xs leading-4">
|
||||
{children}
|
||||
</div>
|
||||
<Tooltip content={I18n.t('workflow_250310_13')}>
|
||||
<div className="leading-none">
|
||||
<IconButton
|
||||
className="ml-0.5"
|
||||
wrapperClass="leading-[0px]"
|
||||
size="mini"
|
||||
icon={<IconCozCopy className="text-xs coz-fg-secondary" />}
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
.item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
|
||||
.empty {
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 272px;
|
||||
min-height: 28px;
|
||||
overflow-y: auto;
|
||||
user-select: text;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--coz-stroke-primary);
|
||||
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
@apply coz-fg-primary;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
border: 0.5px solid var(--coz-stroke-plus);
|
||||
}
|
||||
@@ -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 { useState, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozArrowDown,
|
||||
IconCozArrowUp,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { DataViewer } from '../../data-viewer';
|
||||
import { type FunctionCallLogItem } from '../../../types';
|
||||
import { ContentHeader } from './content-header';
|
||||
|
||||
import styles from './function-call-panel.module.less';
|
||||
|
||||
export const FunctionCallLogPanel: FC<{ item: FunctionCallLogItem }> = ({
|
||||
item,
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center justify-between px-[5px] h-7',
|
||||
styles.header,
|
||||
{
|
||||
[styles['header-expanded']]: !collapsed,
|
||||
},
|
||||
)}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img src={item.icon} className={styles.icon} />
|
||||
<span className="text-xs leading-4 font-medium coz-fg-primary ml-2">
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
{collapsed ? (
|
||||
<IconCozArrowDown className="text-sm coz-fg-secondary"></IconCozArrowDown>
|
||||
) : (
|
||||
<IconCozArrowUp className="text-sm coz-fg-secondary"></IconCozArrowUp>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed ? (
|
||||
<div className={classNames('p-[6px]', styles.content)}>
|
||||
{item.inputs ? (
|
||||
<>
|
||||
<ContentHeader source={item.inputs}>
|
||||
{I18n.t('workflow_250310_11', undefined, '输入')}
|
||||
</ContentHeader>
|
||||
<DataViewer
|
||||
data={item.inputs}
|
||||
className={styles['json-viewer']}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<ContentHeader source={item.outputs} className="mt-1.5">
|
||||
{I18n.t('workflow_250310_12', undefined, '输出')}
|
||||
</ContentHeader>
|
||||
<DataViewer data={item.outputs} className={styles['json-viewer']} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
.container {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { LogWrap } from '../log-wrap';
|
||||
import { type FunctionCallLog } from '../../../types';
|
||||
import { FunctionCallLogPanel } from './function-call-panel';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const FunctionCallLogParser: FC<{ log: FunctionCallLog }> = ({
|
||||
log,
|
||||
}) => {
|
||||
const { items } = log;
|
||||
|
||||
return (
|
||||
<LogWrap
|
||||
label={I18n.t('workflow_250310_06', undefined, '技能调用')}
|
||||
source={log.data}
|
||||
copyable={false}
|
||||
>
|
||||
{items.length ? (
|
||||
<div className={styles.container}>
|
||||
{items.map(item => (
|
||||
<>
|
||||
<FunctionCallLogPanel item={item} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-[1px] border-solid coz-stroke-primary h-7 rounded-[6px]"></div>
|
||||
)}
|
||||
</LogWrap>
|
||||
);
|
||||
};
|
||||
@@ -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 { OutputLogParser } from './output-log-parser';
|
||||
export { ConditionLogParser } from './condition-log-parser';
|
||||
export { NormalLogParser } from './normal-log-parser';
|
||||
export { FunctionCallLogParser } from './function-call-log-parser';
|
||||
export { WorkflowLinkParser } from './workflow-link-parser';
|
||||
@@ -0,0 +1,27 @@
|
||||
.flow-log-detail-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
column-gap: 4px;
|
||||
height: 20px;
|
||||
margin-left: 2px;
|
||||
|
||||
.label-text {
|
||||
color: var(--coz-fg-plus);
|
||||
font-weight: 500;
|
||||
word-break: keep-all;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.coz-icon-button-mini {
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
|
||||
|
||||
import { isObject, toString, isNil } from 'lodash-es';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCopy, IconCozCheckMark } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Toast, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { type LogValueType } from '../../types';
|
||||
|
||||
import styles from './log-wrap.module.less';
|
||||
|
||||
const SPACE = 2;
|
||||
|
||||
export const LogWrap: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
label: string;
|
||||
copyable?: boolean;
|
||||
source?: LogValueType;
|
||||
copyTooltip?: string;
|
||||
labelExtra?: ReactNode;
|
||||
extra?: ReactNode;
|
||||
labelStyle?: React.CSSProperties;
|
||||
}>
|
||||
> = ({
|
||||
label,
|
||||
copyable = true,
|
||||
source,
|
||||
copyTooltip,
|
||||
children,
|
||||
labelExtra,
|
||||
extra,
|
||||
labelStyle,
|
||||
}) => {
|
||||
const [isSuccess, setSuccess] = useState(false);
|
||||
|
||||
const innerCopyable = useMemo(
|
||||
() => copyable && !isNil(source),
|
||||
[copyable, source],
|
||||
);
|
||||
const handleCopy = useCallback(() => {
|
||||
try {
|
||||
const text = isObject(source)
|
||||
? JSON.stringify(source, undefined, SPACE)
|
||||
: toString(source);
|
||||
copy(text);
|
||||
Toast.success({ content: I18n.t('copy_success'), showClose: false });
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
setSuccess(false);
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
Toast.error(I18n.t('copy_failed'));
|
||||
setSuccess(false);
|
||||
}
|
||||
}, [source]);
|
||||
|
||||
const renderCopyButton = () =>
|
||||
isSuccess ? (
|
||||
<Tooltip content={I18n.t('Duplicate_success')}>
|
||||
<IconButton
|
||||
className={'w-[20px] h-[20px] p-[2px]'}
|
||||
size={'mini'}
|
||||
color={'secondary'}
|
||||
icon={<IconCozCheckMark color={'rgba(107, 109, 117, 1)'} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip content={copyTooltip || I18n.t('Copy')}>
|
||||
<IconButton
|
||||
className={'!w-[20px] !h-[20px] !p-[2px] !text-[16px]'}
|
||||
size={'mini'}
|
||||
color={'secondary'}
|
||||
onClick={handleCopy}
|
||||
icon={<IconCozCopy color={'rgba(107, 109, 117, 1)'} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={labelStyle} className={styles['flow-log-detail-label']}>
|
||||
<span className={styles['label-text']}>{label}</span>
|
||||
{innerCopyable ? renderCopyButton() : null}
|
||||
{labelExtra}
|
||||
{extra ? <div className="flex flex-1 justify-end">{extra}</div> : null}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { DataViewer } from '../data-viewer';
|
||||
import { type BaseLog } from '../../types';
|
||||
import { LogWrap } from './log-wrap';
|
||||
|
||||
export const NormalLogParser: React.FC<{ log: BaseLog }> = ({ log }) => (
|
||||
<LogWrap label={log.label} source={log.data} copyTooltip={log.copyTooltip}>
|
||||
<DataViewer data={log.data} emptyPlaceholder={log.emptyPlaceholder} />
|
||||
</LogWrap>
|
||||
);
|
||||
@@ -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 { OutputLogParser } from './output-log-parser';
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 { isEqual, isObject, isUndefined, omit } from 'lodash-es';
|
||||
|
||||
import { type LogValueType } from '../../../types';
|
||||
|
||||
const REASONING_CONTENT_NAME = 'reasoning_content';
|
||||
|
||||
interface isDifferentOutputArgs {
|
||||
/** 节点输出 */
|
||||
nodeOutput: LogValueType;
|
||||
/** 原始输出 */
|
||||
rawOutput: LogValueType;
|
||||
/** 是否是大模型节点 */
|
||||
isLLM?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的节点输出判异
|
||||
*/
|
||||
const isDifferentCommonOutput = (args: isDifferentOutputArgs) => {
|
||||
const { nodeOutput, rawOutput } = args;
|
||||
/**
|
||||
* case1: rawOutput === undefined
|
||||
* 可以无须比较,直接返回假
|
||||
*/
|
||||
if (isUndefined(rawOutput)) {
|
||||
return false;
|
||||
}
|
||||
const nodeOutputType = typeof nodeOutput;
|
||||
const rawOutputType = typeof rawOutput;
|
||||
/** case2: 两者类型不同 */
|
||||
if (nodeOutputType !== rawOutputType) {
|
||||
return true;
|
||||
}
|
||||
/** case4: 深度比较 */
|
||||
return !isEqual(nodeOutput, rawOutput);
|
||||
};
|
||||
|
||||
/**
|
||||
* 大模型节点特有的判断逻辑
|
||||
*/
|
||||
const isDifferentLLMOutput = (args: isDifferentOutputArgs) => {
|
||||
const { nodeOutput } = args;
|
||||
|
||||
/** 如果节点输出是对象,则去除系统字段 */
|
||||
const readNodeOutput = isObject(nodeOutput)
|
||||
? omit(nodeOutput, [REASONING_CONTENT_NAME])
|
||||
: nodeOutput;
|
||||
const isDiffCommon = isDifferentCommonOutput({
|
||||
...args,
|
||||
nodeOutput: readNodeOutput,
|
||||
});
|
||||
/** 常规判断已经判同,则直接返回 */
|
||||
if (!isDiffCommon) {
|
||||
return isDiffCommon;
|
||||
}
|
||||
/** 如果不是节点输出不是对象,直接判异 */
|
||||
if (!isObject(readNodeOutput)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const arr = Object.entries(readNodeOutput);
|
||||
/** 如果排除系统字段,仍然超过多个字段,则无须进一步比较,直接判异 */
|
||||
if (arr.length !== 1) {
|
||||
return true;
|
||||
}
|
||||
/** 用唯一的值与节点输出做异同判断 */
|
||||
return isDifferentCommonOutput({
|
||||
...args,
|
||||
nodeOutput: arr[0][1],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 精细的判断节点输出和原始输出是否相同
|
||||
*/
|
||||
export const isDifferentOutput = (
|
||||
args: isDifferentOutputArgs,
|
||||
): [boolean, any] => {
|
||||
/**
|
||||
* nodeOutput 可能值:
|
||||
* 1. undefined
|
||||
* 2. string
|
||||
* 3. 包涵一个自定义字段、reasoning_content、LogObjSpecialKey
|
||||
* 4. 包涵多个自定义字段
|
||||
* rawOutput 可能值:
|
||||
* 1. undefined
|
||||
* 2. string
|
||||
* 4. 任意对象
|
||||
* 5. 任意数组
|
||||
*/
|
||||
try {
|
||||
const { isLLM } = args;
|
||||
const result = isLLM
|
||||
? isDifferentLLMOutput(args)
|
||||
: isDifferentCommonOutput(args);
|
||||
return [result, undefined];
|
||||
} catch (err) {
|
||||
/** 该函数会深入解析日志结构,不排除出现异常的可能性,出现异常则判异, */
|
||||
return [true, err];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
.json-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.output-log {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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 { isObject } from 'lodash-es';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { NodeExeStatus } from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozWarningCircle } from '@coze-arch/coze-design/icons';
|
||||
import { SegmentTab, Tag, Typography, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { LogWrap } from '../log-wrap';
|
||||
import { DataViewer } from '../../data-viewer';
|
||||
import { type OutputLog } from '../../../types';
|
||||
import { useOutputLog, TabValue } from './use-output-log';
|
||||
import { SyncOutputToNode } from './sync-output-to-node';
|
||||
|
||||
import css from './output-log-parser.module.less';
|
||||
|
||||
const MockInfo: React.FC<{ log: OutputLog }> = ({ log }) => {
|
||||
const { mockInfo } = log;
|
||||
|
||||
if (!mockInfo?.isHit) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag size="mini" style={{ maxWidth: '100px' }}>
|
||||
<Typography.Text ellipsis={{ showTooltip: true }} size="small">
|
||||
{I18n.t('mockset')}:{mockInfo?.mockSetName}
|
||||
</Typography.Text>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const LLMTabTooltip = () => (
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<Typography.Text fontSize="14px">
|
||||
{I18n.t('wf_testrun_log_md_llm_diff_tooltip')}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
fontSize="14px"
|
||||
link={{
|
||||
href: '/open/docs/guides/llm_node#f1e97a47',
|
||||
target: '_blank',
|
||||
}}
|
||||
>
|
||||
{I18n.t('wf_testrun_log_md_llm_diff_tooltip_a')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconCozWarningCircle />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const OutputLogParser: React.FC<{
|
||||
log: OutputLog;
|
||||
node?: FlowNodeEntity;
|
||||
nodeStatus?: NodeExeStatus;
|
||||
onPreview?: (value: string, path: string[]) => void;
|
||||
}> = ({ log, node, nodeStatus, onPreview }) => {
|
||||
const { showRawOutput, tab, data, options, setTab } = useOutputLog(log);
|
||||
|
||||
const isLLM = log.nodeType === 'LLM';
|
||||
|
||||
const showCodeSync =
|
||||
node?.flowNodeType === StandardNodeType.Code &&
|
||||
nodeStatus === NodeExeStatus.Success &&
|
||||
isObject(log.rawOutput?.data);
|
||||
|
||||
const isFinished =
|
||||
nodeStatus === NodeExeStatus.Success || nodeStatus === NodeExeStatus.Fail;
|
||||
|
||||
return (
|
||||
<LogWrap
|
||||
label={log.label}
|
||||
source={data}
|
||||
copyTooltip={log.copyTooltip}
|
||||
labelExtra={<MockInfo log={log} />}
|
||||
extra={
|
||||
<div className={css.extra}>
|
||||
{showCodeSync ? (
|
||||
<SyncOutputToNode
|
||||
node={node}
|
||||
output={log.rawOutput?.data as object}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={css['output-log']}>
|
||||
{showRawOutput ? (
|
||||
<SegmentTab
|
||||
size="small"
|
||||
value={tab}
|
||||
onChange={e => {
|
||||
setTab(e.target.value);
|
||||
}}
|
||||
>
|
||||
{options.map(i => (
|
||||
<SegmentTab.Tab value={i.value}>
|
||||
<span className={css.tab}>
|
||||
{i.label}
|
||||
{isLLM && i.value === TabValue.RawOutput ? (
|
||||
<LLMTabTooltip />
|
||||
) : null}
|
||||
</span>
|
||||
</SegmentTab.Tab>
|
||||
))}
|
||||
</SegmentTab>
|
||||
) : null}
|
||||
<DataViewer
|
||||
data={data}
|
||||
mdPreview={isFinished}
|
||||
onPreview={onPreview}
|
||||
className="!min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</LogWrap>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { isInteger } from 'lodash-es';
|
||||
import { ViewVariableType } from '@coze-workflow/base/types';
|
||||
|
||||
interface BotsParam {
|
||||
name: string;
|
||||
type: ViewVariableType;
|
||||
children?: Array<BotsParam>;
|
||||
}
|
||||
|
||||
export enum ConvertSchemaErrorCode {
|
||||
MaxDepthExceeded = 0,
|
||||
ContainsInvalidValue,
|
||||
}
|
||||
|
||||
export class ConvertSchemaError extends Error {
|
||||
errorType: ConvertSchemaErrorCode;
|
||||
constructor(errorType: ConvertSchemaErrorCode, message?: string) {
|
||||
super(message);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
}
|
||||
|
||||
export function convertSchema<T extends Object>(
|
||||
object: T,
|
||||
maxDepth = 20,
|
||||
currentDepth = 1,
|
||||
): BotsParam[] {
|
||||
if (currentDepth > maxDepth) {
|
||||
throw new ConvertSchemaError(
|
||||
ConvertSchemaErrorCode.MaxDepthExceeded,
|
||||
'Max depth exceeded',
|
||||
);
|
||||
}
|
||||
const paramSchema: BotsParam[] = [];
|
||||
|
||||
// if (object === null) {
|
||||
// throw new ConvertSchemaError(
|
||||
// ConvertSchemaErrorCode.ContainsInvalidValue,
|
||||
// 'ContainsInvalidValue',
|
||||
// );
|
||||
// }
|
||||
|
||||
Object.keys(object).forEach(key => {
|
||||
const value: unknown = (object as any)[key];
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.String,
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
if (isInteger(value)) {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.Integer,
|
||||
});
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.Number,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.Boolean,
|
||||
});
|
||||
break;
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
// omit null values
|
||||
break;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
switch (typeof value[0]) {
|
||||
case 'string':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayString,
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
if (isInteger(value[0])) {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayInteger,
|
||||
});
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayNumber,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayBoolean,
|
||||
});
|
||||
break;
|
||||
case 'object':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayObject,
|
||||
children: convertSchema(value[0], maxDepth, currentDepth + 1),
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayString,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.ArrayString,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
type: ViewVariableType.Object,
|
||||
children: convertSchema(value, maxDepth, currentDepth + 1),
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('value,to default', value);
|
||||
throw new ConvertSchemaError(
|
||||
ConvertSchemaErrorCode.ContainsInvalidValue,
|
||||
'ContainsInvalidValue',
|
||||
);
|
||||
}
|
||||
});
|
||||
return paramSchema;
|
||||
}
|
||||
@@ -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 { type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozUpdate } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { useSyncOutput } from './use-sync-output';
|
||||
import { convertSchema } from './convert';
|
||||
|
||||
export const SyncOutputToNode: FC<{
|
||||
output: object;
|
||||
node: FlowNodeEntity;
|
||||
}> = props => {
|
||||
const { output, node } = props;
|
||||
|
||||
const updateOutput = useSyncOutput('/outputs', node);
|
||||
|
||||
const handleUpdateOutput = () => {
|
||||
const outputSchema = convertSchema(output);
|
||||
updateOutput(outputSchema);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="highlight"
|
||||
size="mini"
|
||||
icon={<IconCozUpdate />}
|
||||
onClick={handleUpdateOutput}
|
||||
>
|
||||
{I18n.t('workflow_code_testrun_sync')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 { nanoid } from 'nanoid';
|
||||
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type ViewVariableTreeNode,
|
||||
type ViewVariableType,
|
||||
type ValueExpression,
|
||||
} from '@coze-workflow/variable';
|
||||
|
||||
export type InputParams = Array<{
|
||||
name: string;
|
||||
input: ValueExpression;
|
||||
}>;
|
||||
export type OutputParams = ViewVariableTreeNode[];
|
||||
export interface ParsedOutput {
|
||||
name: string;
|
||||
type: ViewVariableType;
|
||||
children?: ParsedOutput[];
|
||||
}
|
||||
|
||||
export interface ParsedOutputWithKey extends ParsedOutput {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
const turnOutputParamsToMapDeep = (outputParams: OutputParams) => {
|
||||
const outputParamsMap: Record<string, ViewVariableTreeNode> = {};
|
||||
|
||||
const recursiveLoopOutputParams = (output: OutputParams) => {
|
||||
output.forEach(item => {
|
||||
outputParamsMap[item.name] = item;
|
||||
if (item.children) {
|
||||
recursiveLoopOutputParams(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
recursiveLoopOutputParams(outputParams);
|
||||
|
||||
return outputParamsMap;
|
||||
};
|
||||
|
||||
const updateOutputWithNewType = (
|
||||
outputParams: OutputParams,
|
||||
parsedOutput: ParsedOutput[],
|
||||
) => {
|
||||
const oldOutputMap = turnOutputParamsToMapDeep(outputParams);
|
||||
|
||||
const loopParsedOutput = (items: ParsedOutput[]) =>
|
||||
items.map(item => {
|
||||
const newItem: ParsedOutputWithKey = { ...item };
|
||||
|
||||
if (oldOutputMap[item.name]) {
|
||||
newItem.key = oldOutputMap[item.name].key;
|
||||
} else {
|
||||
newItem.key = nanoid();
|
||||
}
|
||||
|
||||
if (newItem.children) {
|
||||
newItem.children = loopParsedOutput(newItem.children);
|
||||
}
|
||||
|
||||
return newItem;
|
||||
});
|
||||
|
||||
return loopParsedOutput(parsedOutput);
|
||||
};
|
||||
|
||||
export const useSyncOutput = (outputPath: string, node: FlowNodeEntity) => {
|
||||
// TODO: 改到 effects 中实现 ,依赖节点引擎支持自定义事件触发 effects
|
||||
const updateOutput = (output: ParsedOutput[]) => {
|
||||
if (outputPath) {
|
||||
const formModel =
|
||||
node?.getData<FlowNodeFormData>(FlowNodeFormData).formModel;
|
||||
const outputFormItem = formModel?.getFormItemByPath(outputPath);
|
||||
if (outputFormItem) {
|
||||
outputFormItem.value = updateOutputWithNewType(
|
||||
outputFormItem.value,
|
||||
output,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return updateOutput;
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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, useMemo } from 'react';
|
||||
|
||||
import { toString } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type OutputLog } from '../../../types';
|
||||
import { useTestRunReporterService } from '../../../../../hooks';
|
||||
import { isDifferentOutput } from './is-different-output';
|
||||
|
||||
const CODE_TEXT = {
|
||||
tabLabel: I18n.t('workflow_detail_testrun_panel_raw_output_code'),
|
||||
};
|
||||
const LLM_TEXT = {
|
||||
tabLabel: I18n.t('workflow_detail_testrun_panel_raw_output_llm'),
|
||||
};
|
||||
const DEFAULT_TEXT = {
|
||||
tabLabel: I18n.t('workflow_detail_testrun_panel_raw_output'),
|
||||
};
|
||||
/** 一些特化节点的文案 */
|
||||
const TEXT = {
|
||||
Code: CODE_TEXT,
|
||||
LLM: LLM_TEXT,
|
||||
};
|
||||
|
||||
export enum TabValue {
|
||||
Output,
|
||||
RawOutput,
|
||||
}
|
||||
|
||||
export const useOutputLog = (log: OutputLog) => {
|
||||
const [tab, setTab] = useState(TabValue.Output);
|
||||
const reporter = useTestRunReporterService();
|
||||
/** 是否渲染原始输出 */
|
||||
const showRawOutput = useMemo(() => {
|
||||
const [result, err] = isDifferentOutput({
|
||||
nodeOutput: log.data,
|
||||
rawOutput: log.rawOutput?.data,
|
||||
isLLM: log.nodeType === 'LLM',
|
||||
});
|
||||
reporter.logRawOutputDifference({
|
||||
is_difference: result,
|
||||
error_msg: err ? toString(err) : undefined,
|
||||
log_node_type: log.nodeType,
|
||||
});
|
||||
return result;
|
||||
}, [log]);
|
||||
|
||||
const text = useMemo(() => TEXT[log.nodeType] || DEFAULT_TEXT, [log]);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: TabValue.Output,
|
||||
label: I18n.t('workflow_detail_testrun_panel_final_output2'),
|
||||
},
|
||||
{
|
||||
value: TabValue.RawOutput,
|
||||
label: text.tabLabel,
|
||||
},
|
||||
],
|
||||
[text],
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() => (tab === TabValue.Output ? log.data : log.rawOutput?.data),
|
||||
[tab, log],
|
||||
);
|
||||
|
||||
return {
|
||||
showRawOutput,
|
||||
options,
|
||||
text,
|
||||
tab,
|
||||
data,
|
||||
setTab,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { type WorkflowLinkLogData } from '@/types';
|
||||
|
||||
import { type WorkflowLinkLog } from '../../types';
|
||||
|
||||
export const WorkflowLinkParser: React.FC<{
|
||||
log: WorkflowLinkLog;
|
||||
onOpenWorkflowLink?: (data: WorkflowLinkLogData) => any;
|
||||
}> = ({ log, onOpenWorkflowLink }) => (
|
||||
<div className="flex items-center">
|
||||
<span className="mr-[16px] text-[14px] coz-fg-plus font-medium">
|
||||
{log.label}
|
||||
</span>
|
||||
<Typography.Text
|
||||
size="small"
|
||||
link
|
||||
onClick={() => onOpenWorkflowLink?.(log.data)}
|
||||
>
|
||||
{I18n.t('View')}
|
||||
</Typography.Text>
|
||||
</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 { useMarkdownModal } from './use-markdown-modal';
|
||||
@@ -0,0 +1,3 @@
|
||||
.markdown-modal {
|
||||
min-height: 520px;
|
||||
}
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import { MarkdownBoxViewer } from './markdown-viewer';
|
||||
|
||||
import css from './markdown-modal.module.less';
|
||||
|
||||
interface MarkdownModalProps {
|
||||
visible?: boolean;
|
||||
value: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MarkdownModal: React.FC<MarkdownModalProps> = ({
|
||||
visible,
|
||||
value,
|
||||
onClose,
|
||||
}) => (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={I18n.t('creat_project_use_template_preview')}
|
||||
size="large"
|
||||
getPopupContainer={() => document.body}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<MarkdownBoxViewer value={value} className={css['markdown-modal']} />
|
||||
</Modal>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
.md-box-viewer :global(.flow-markdown-body ) {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
color: var(--coz-fg-primary);
|
||||
}
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h5, h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
:global(.paragraph-element) {
|
||||
color: var(--coz-fg-primary);
|
||||
}
|
||||
blockquote {
|
||||
h1, h2, h3, h4, h5, h6, :global(.paragraph-element) {
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cls from 'classnames';
|
||||
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
|
||||
import css from './markdown-viewer.module.less';
|
||||
|
||||
interface MarkdownViewerProps {
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MarkdownBoxViewer: React.FC<MarkdownViewerProps> = ({
|
||||
value,
|
||||
className,
|
||||
}) => (
|
||||
<div className={cls(css['md-box-viewer'], className)}>
|
||||
<MdBoxLazy markDown={value} />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { MarkdownModal } from './markdown-modal';
|
||||
|
||||
export const useMarkdownModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const open = (nextValue: string) => {
|
||||
setValue(nextValue);
|
||||
setVisible(true);
|
||||
};
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
setValue('');
|
||||
};
|
||||
|
||||
const modal = useMemo(
|
||||
() =>
|
||||
value ? (
|
||||
<MarkdownModal visible={visible} value={value} onClose={close} />
|
||||
) : null,
|
||||
[visible, value],
|
||||
);
|
||||
|
||||
return {
|
||||
open,
|
||||
modal,
|
||||
};
|
||||
};
|
||||
@@ -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 { NodeStatusBar } from './node-status-bar';
|
||||
@@ -0,0 +1,52 @@
|
||||
.node-status-bar {
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
border-radius: 8px;
|
||||
background-color: var(--coz-bg-max);
|
||||
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px;
|
||||
|
||||
&-opened {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
:global {
|
||||
.coz-tag {
|
||||
height: 20px;
|
||||
}
|
||||
.semi-tag-content {
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.semi-tag-suffix-icon >div{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.status-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
}
|
||||
|
||||
.is-show-detail {
|
||||
transform: rotate(180deg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowDown } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { useNodeRender } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import styles from './node-status-bar.module.less';
|
||||
|
||||
interface NodeStatusBarProps {
|
||||
header?: React.ReactNode;
|
||||
defaultShowDetail?: boolean;
|
||||
hasExecuteResult?: boolean;
|
||||
needAuth?: boolean;
|
||||
/**
|
||||
* 是否包含会话处理
|
||||
*/
|
||||
hasConversation?: boolean;
|
||||
onAuth?: () => void;
|
||||
onJumpToProjectConversation?: () => void;
|
||||
extraBtns?: React.ReactNode[];
|
||||
}
|
||||
|
||||
export const NodeStatusBar: React.FC<
|
||||
React.PropsWithChildren<NodeStatusBarProps>
|
||||
> = ({
|
||||
header,
|
||||
defaultShowDetail,
|
||||
hasExecuteResult,
|
||||
needAuth,
|
||||
onAuth,
|
||||
hasConversation,
|
||||
onJumpToProjectConversation,
|
||||
children,
|
||||
extraBtns = [],
|
||||
}) => {
|
||||
const [showDetail, setShowDetail] = useState(defaultShowDetail);
|
||||
const { selectNode } = useNodeRender();
|
||||
|
||||
const handleAuth = e => {
|
||||
e.stopPropagation();
|
||||
selectNode(e);
|
||||
onAuth?.();
|
||||
};
|
||||
const handleToggleShowDetail = e => {
|
||||
e.stopPropagation();
|
||||
selectNode(e);
|
||||
setShowDetail(!showDetail);
|
||||
};
|
||||
const handleConversation = e => {
|
||||
e.stopPropagation();
|
||||
selectNode(e);
|
||||
onJumpToProjectConversation?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles['node-status-bar']}
|
||||
// 必须要禁止 down 冒泡,防止判定圈选和 node hover(不支持多边形)
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
// 其他事件统一走点击事件,且也需要阻止冒泡
|
||||
onClick={handleToggleShowDetail}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles['status-header'], {
|
||||
[styles['status-header-opened']]: showDetail,
|
||||
})}
|
||||
>
|
||||
<div className={styles['status-title']}>
|
||||
{header}
|
||||
{extraBtns.length > 0 ? extraBtns : null}
|
||||
{needAuth ? (
|
||||
<Button size="small" color="secondary" onClick={handleAuth}>
|
||||
{I18n.t('knowledge_feishu_10')}
|
||||
</Button>
|
||||
) : null}
|
||||
{hasConversation ? (
|
||||
<Button size="small" color="secondary" onClick={handleConversation}>
|
||||
{I18n.t('workflow_view_data')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles['status-btns']}>
|
||||
{hasExecuteResult ? (
|
||||
<IconCozArrowDown
|
||||
className={classNames({
|
||||
[styles['is-show-detail']]: showDetail,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{showDetail ? children : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { merge } from 'lodash-es';
|
||||
import { ConditionType } from '@coze-workflow/base/api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const conditionRelationField = 'relation';
|
||||
export const referenceNameField = 'param';
|
||||
export const conditionField = 'conditionType';
|
||||
export const compareValueField = 'value';
|
||||
export const isQuotedField = 'isQuote';
|
||||
export const quotedField = 'quotedValue';
|
||||
export const fixedField = 'fixedValue';
|
||||
export const compareIsQuotedField = `${compareValueField}.${isQuotedField}`;
|
||||
export const compareQuotedField = `${compareValueField}.${quotedField}`;
|
||||
export const compareFixedField = `${compareValueField}.${fixedField}`;
|
||||
|
||||
export const equalValue = {
|
||||
[ConditionType.Equal]: () => I18n.t('workflow_detail_condition_select_equal'),
|
||||
};
|
||||
export const notEqualValue = {
|
||||
[ConditionType.NotEqual]: () =>
|
||||
I18n.t('workflow_detail_condition_select_not_equal'),
|
||||
};
|
||||
export const lengthBiggerValue = {
|
||||
[ConditionType.LengthGt]: () =>
|
||||
I18n.t('workflow_detail_condition_select_longer'),
|
||||
};
|
||||
export const lengthBiggerEqualValue = {
|
||||
[ConditionType.LengthGtEqual]: () =>
|
||||
I18n.t('workflow_detail_condition_select_longer_equal'),
|
||||
};
|
||||
export const lengthSmallerValue = {
|
||||
[ConditionType.LengthLt]: () =>
|
||||
I18n.t('workflow_detail_condition_select_shorter'),
|
||||
};
|
||||
export const lengthSmallerEqualValue = {
|
||||
[ConditionType.LengthLtEqual]: () =>
|
||||
I18n.t('workflow_detail_condition_select_shorter_equal'),
|
||||
};
|
||||
export const includeValue = {
|
||||
[ConditionType.Contains]: () =>
|
||||
I18n.t('workflow_detail_condition_select_contain'),
|
||||
};
|
||||
export const excludeValue = {
|
||||
[ConditionType.NotContains]: () =>
|
||||
I18n.t('workflow_detail_condition_select_not_contain'),
|
||||
};
|
||||
export const emptyValue = {
|
||||
[ConditionType.Null]: () => I18n.t('workflow_detail_condition_select_empty'),
|
||||
};
|
||||
export const notEmptyValue = {
|
||||
[ConditionType.NotNull]: () =>
|
||||
I18n.t('workflow_detail_condition_select_not_empty'),
|
||||
};
|
||||
export const biggerValue = {
|
||||
[ConditionType.Gt]: () => I18n.t('workflow_detail_condition_select_greater'),
|
||||
};
|
||||
export const biggerEqualValue = {
|
||||
[ConditionType.GtEqual]: () =>
|
||||
I18n.t('workflow_detail_condition_select_greater_equal'),
|
||||
};
|
||||
export const smallerValue = {
|
||||
[ConditionType.Lt]: () => I18n.t('workflow_detail_condition_select_less'),
|
||||
};
|
||||
export const smallerEqualValue = {
|
||||
[ConditionType.LtEqual]: () =>
|
||||
I18n.t('workflow_detail_condition_select_less_equal'),
|
||||
};
|
||||
export const trueValue = {
|
||||
[ConditionType.True]: () => I18n.t('workflow_detail_condition_select_true'),
|
||||
};
|
||||
export const falseValue = {
|
||||
[ConditionType.False]: () => I18n.t('workflow_detail_condition_select_false'),
|
||||
};
|
||||
|
||||
// 等于、不等于、长度大于、长度大于等于、长度小于、长度小于等于、包含、不包含、为空、不为空
|
||||
export const stringConditionValueMap = merge(
|
||||
{},
|
||||
equalValue,
|
||||
notEqualValue,
|
||||
lengthBiggerValue,
|
||||
lengthBiggerEqualValue,
|
||||
lengthSmallerValue,
|
||||
lengthSmallerEqualValue,
|
||||
includeValue,
|
||||
excludeValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
);
|
||||
|
||||
// 等于、不等于、大于、大于等于、小于、小于等于、为空、不为空
|
||||
export const intConditionValueMap = merge(
|
||||
{},
|
||||
equalValue,
|
||||
notEqualValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
biggerValue,
|
||||
biggerEqualValue,
|
||||
smallerValue,
|
||||
smallerEqualValue,
|
||||
);
|
||||
|
||||
// 等于、不等于、为True、为False、为空、不为空
|
||||
export const booleanConditionValueMap = merge(
|
||||
{},
|
||||
equalValue,
|
||||
notEqualValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
trueValue,
|
||||
falseValue,
|
||||
);
|
||||
|
||||
// 等于、不等于、大于等于、小于等于、大于、小于、为空、不为空
|
||||
export const numberConditionValueMap = merge(
|
||||
{},
|
||||
equalValue,
|
||||
notEqualValue,
|
||||
biggerValue,
|
||||
biggerEqualValue,
|
||||
smallerValue,
|
||||
smallerEqualValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
);
|
||||
|
||||
// 包含、不包含、为空、不为空
|
||||
export const objectConditionValueMap = merge(
|
||||
{},
|
||||
includeValue,
|
||||
excludeValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
);
|
||||
|
||||
// 长度大于、长度大于等于、长度小于、长度小于等于、包含、不包含、为空、不为空
|
||||
export const arrayConditionValueMap = merge(
|
||||
{},
|
||||
lengthBiggerValue,
|
||||
lengthBiggerEqualValue,
|
||||
lengthSmallerValue,
|
||||
lengthSmallerEqualValue,
|
||||
includeValue,
|
||||
excludeValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
);
|
||||
|
||||
// 所有的值的集合
|
||||
export const totalConditionValueMap = merge(
|
||||
{},
|
||||
equalValue,
|
||||
notEqualValue,
|
||||
lengthBiggerValue,
|
||||
lengthBiggerEqualValue,
|
||||
lengthSmallerValue,
|
||||
lengthSmallerEqualValue,
|
||||
includeValue,
|
||||
excludeValue,
|
||||
emptyValue,
|
||||
notEmptyValue,
|
||||
biggerValue,
|
||||
biggerEqualValue,
|
||||
smallerValue,
|
||||
smallerEqualValue,
|
||||
trueValue,
|
||||
falseValue,
|
||||
);
|
||||
|
||||
export const fileConditionValueMap = merge({}, notEmptyValue, emptyValue);
|
||||
|
||||
export enum ConditionRightType {
|
||||
Ref = 'ref',
|
||||
Literal = 'literal',
|
||||
}
|
||||
|
||||
export enum Logic {
|
||||
OR = 1,
|
||||
AND = 2,
|
||||
}
|
||||
|
||||
export const logicTextMap = new Map<number, string>([
|
||||
[Logic.OR, I18n.t('workflow_detail_condition_or')],
|
||||
[Logic.AND, I18n.t('workflow_detail_condition_and')],
|
||||
]);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export {
|
||||
totalConditionValueMap,
|
||||
ConditionRightType,
|
||||
logicTextMap,
|
||||
} from './condition';
|
||||
|
||||
/** 日志类型 */
|
||||
export enum LogType {
|
||||
/** 输入 */
|
||||
Input,
|
||||
/** 输出 */
|
||||
Output,
|
||||
/** 批处理数据 */
|
||||
Batch,
|
||||
/** Condition */
|
||||
Condition,
|
||||
/** 大模型推理过程 */
|
||||
Reasoning,
|
||||
/** 大模型Function过程 */
|
||||
FunctionCall,
|
||||
/** 子流程跳转连接 */
|
||||
WorkflowLink,
|
||||
}
|
||||
|
||||
export enum EndTerminalPlan {
|
||||
Variable = 1,
|
||||
Text = 2,
|
||||
}
|
||||
@@ -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 { isObject, toString } from 'lodash-es';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
const SPACE = 2;
|
||||
|
||||
export function useCopy(source: unknown) {
|
||||
const handleCopy = useCallback(() => {
|
||||
try {
|
||||
const text = isObject(source)
|
||||
? JSON.stringify(source, undefined, SPACE)
|
||||
: toString(source);
|
||||
copy(text);
|
||||
Toast.success({ content: I18n.t('copy_success'), showClose: false });
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
Toast.error(I18n.t('copy_failed'));
|
||||
}
|
||||
}, [source]);
|
||||
|
||||
return {
|
||||
handleCopy,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { NodeStatusBar } from './components/node-status-bar';
|
||||
export { LogImages } from './components/log-images';
|
||||
export { DataViewer } from './components/data-viewer';
|
||||
export { useMarkdownModal } from './components/markdown-viewer';
|
||||
export {
|
||||
ConditionLogParser,
|
||||
OutputLogParser,
|
||||
NormalLogParser,
|
||||
FunctionCallLogParser,
|
||||
WorkflowLinkParser,
|
||||
} from './components/log-parser';
|
||||
|
||||
export { LogType } from './constants';
|
||||
export { generateLog } from './utils/generate-log';
|
||||
export {
|
||||
isConditionLog,
|
||||
isOutputLog,
|
||||
isReasoningLog,
|
||||
isFunctionCallLog,
|
||||
isWorkflowLinkLog,
|
||||
} from './utils/field';
|
||||
|
||||
export { Log, ConditionLog } from './types';
|
||||
104
frontend/packages/workflow/test-run/src/features/log/types.ts
Normal file
104
frontend/packages/workflow/test-run/src/features/log/types.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 WorkflowLinkLogData } from '@/types';
|
||||
|
||||
import { type LogType } from './constants';
|
||||
|
||||
/** log 中的 value 可能值 */
|
||||
export type LogValueType =
|
||||
| string
|
||||
| null
|
||||
| number
|
||||
| object
|
||||
| boolean
|
||||
| undefined;
|
||||
|
||||
/** 通常的日志结构 */
|
||||
export interface BaseLog {
|
||||
label: string;
|
||||
data: LogValueType;
|
||||
copyTooltip?: string;
|
||||
type: LogType;
|
||||
emptyPlaceholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* condition 的值
|
||||
*/
|
||||
export interface ConditionData {
|
||||
leftData: LogValueType;
|
||||
rightData: LogValueType;
|
||||
operatorData: string;
|
||||
}
|
||||
export interface ConditionGroup {
|
||||
conditions: ConditionData[];
|
||||
name: string;
|
||||
logic: number;
|
||||
logicData: string;
|
||||
}
|
||||
|
||||
/** condition 的日志结构 */
|
||||
export interface ConditionLog {
|
||||
conditions: ConditionGroup[];
|
||||
type: LogType.Condition;
|
||||
}
|
||||
|
||||
export interface FunctionCallLogItem {
|
||||
name: string;
|
||||
inputs?: Record<string, unknown>;
|
||||
outputs: string | Record<string, unknown>;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface FunctionCallLog {
|
||||
type: LogType.FunctionCall;
|
||||
items: FunctionCallLogItem[];
|
||||
copyTooltip?: string;
|
||||
data: LogValueType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出的日志结构
|
||||
*/
|
||||
export interface OutputLog {
|
||||
label: string;
|
||||
data: LogValueType;
|
||||
copyTooltip?: string;
|
||||
type: LogType.Output;
|
||||
/** 节点类型 */
|
||||
nodeType: string;
|
||||
mockInfo?: {
|
||||
isHit: boolean;
|
||||
mockSetName?: string;
|
||||
};
|
||||
rawOutput?: {
|
||||
data: LogValueType;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkflowLinkLog {
|
||||
type: LogType.WorkflowLink;
|
||||
label: string;
|
||||
data: WorkflowLinkLogData;
|
||||
}
|
||||
|
||||
export type Log =
|
||||
| BaseLog
|
||||
| ConditionLog
|
||||
| OutputLog
|
||||
| FunctionCallLog
|
||||
| WorkflowLinkLog;
|
||||
@@ -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 {
|
||||
type Log,
|
||||
type ConditionLog,
|
||||
type OutputLog,
|
||||
type BaseLog,
|
||||
type FunctionCallLog,
|
||||
type WorkflowLinkLog,
|
||||
} from '../types';
|
||||
import { LogType } from '../constants';
|
||||
|
||||
/** 是否是输出日志 */
|
||||
export const isOutputLog = (log: Log): log is OutputLog =>
|
||||
log.type === LogType.Output;
|
||||
|
||||
/** 是否是 condition 输入 */
|
||||
export const isConditionLog = (log: Log): log is ConditionLog =>
|
||||
log.type === LogType.Condition;
|
||||
|
||||
/** 是否是大模型推理日志 */
|
||||
export const isReasoningLog = (log: Log): log is BaseLog =>
|
||||
log.type === LogType.Reasoning;
|
||||
|
||||
export const isFunctionCallLog = (log: Log): log is FunctionCallLog =>
|
||||
log.type === LogType.FunctionCall;
|
||||
|
||||
/** 是否是子流程跳转连接 */
|
||||
export const isWorkflowLinkLog = (log: Log): log is WorkflowLinkLog =>
|
||||
log.type === LogType.WorkflowLink;
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type FunctionCallLog, type BaseLog, type Log } from '../types';
|
||||
import { LogType } from '../constants';
|
||||
import {
|
||||
type FunctionCallDetail,
|
||||
parseFunctionCall,
|
||||
} from './parse-function-call';
|
||||
|
||||
export function generateLLMOutput(
|
||||
logs: Log[],
|
||||
responseExtra: Record<string, unknown>,
|
||||
node?: FlowNodeEntity,
|
||||
) {
|
||||
const {
|
||||
reasoning_content: reasoningContent,
|
||||
fc_called_detail: fcCalledDetail,
|
||||
} = responseExtra;
|
||||
if (reasoningContent) {
|
||||
const reasoningLog: BaseLog = {
|
||||
type: LogType.Reasoning,
|
||||
label: I18n.t('workflow_250217_01'),
|
||||
data: reasoningContent,
|
||||
copyTooltip: I18n.t('workflow_detail_title_testrun_copyoutput'),
|
||||
};
|
||||
logs.push(reasoningLog);
|
||||
}
|
||||
|
||||
if (fcCalledDetail) {
|
||||
const reasoningLog: FunctionCallLog = parseFunctionCall(
|
||||
fcCalledDetail as FunctionCallDetail,
|
||||
node,
|
||||
);
|
||||
logs.push(reasoningLog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* 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 complexity */
|
||||
|
||||
import { isFunction, isString, isObject } from 'lodash-es';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowNodeData } from '@coze-workflow/nodes';
|
||||
import type { NodeResult } from '@coze-workflow/base/api';
|
||||
import { type StandardNodeType } from '@coze-workflow/base';
|
||||
import { LogObjSpecialKey } from '@coze-common/json-viewer';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { MockHitStatus } from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import { type Log, type LogValueType, type OutputLog } from '../types';
|
||||
import {
|
||||
totalConditionValueMap,
|
||||
ConditionRightType,
|
||||
logicTextMap,
|
||||
EndTerminalPlan,
|
||||
LogType,
|
||||
} from '../constants';
|
||||
import { typeSafeJSONParse } from '../../../utils';
|
||||
import { generateLLMOutput } from './generate-llm-output';
|
||||
|
||||
const enableBigInt = true;
|
||||
const hasStringOutput = (rawData: unknown) =>
|
||||
isString(rawData) && rawData.length > 0;
|
||||
|
||||
const normalizeConditionInputData = (inputData: any) => {
|
||||
try {
|
||||
const { branches, conditions, logic } = inputData;
|
||||
if (branches) {
|
||||
return branches;
|
||||
}
|
||||
if (conditions) {
|
||||
return [
|
||||
{
|
||||
conditions,
|
||||
logic,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const generateBatchData = (result: NodeResult): Log => {
|
||||
const batchData =
|
||||
typeSafeJSONParse(result.items, {
|
||||
enableBigInt,
|
||||
}) || {};
|
||||
return {
|
||||
label: I18n.t('workflow_detail_testrun_panel_batch_value'),
|
||||
data: batchData,
|
||||
copyTooltip: I18n.t('workflow_detail_title_testrun_copy_batch'),
|
||||
type: LogType.Batch,
|
||||
};
|
||||
};
|
||||
|
||||
const generateInput = (logs: Log[], result: NodeResult) => {
|
||||
const { NodeType: type } = result;
|
||||
/** input 不可能是 string,所以使用 {} 兜底,异常情况直接展示空即可 */
|
||||
const inputData = (typeSafeJSONParse(result.input, {
|
||||
emptyValue: result.input,
|
||||
enableBigInt,
|
||||
}) || {}) as LogValueType;
|
||||
/** step 1.1: condition 节点单独处理 */
|
||||
if (type === 'If') {
|
||||
const normalizeConditions = normalizeConditionInputData(inputData);
|
||||
const conditions = normalizeConditions.map(branch => ({
|
||||
conditions: branch.conditions.map(condition => {
|
||||
/** 右值不一定存在 */
|
||||
const { left, right = {}, operator } = condition;
|
||||
const operatorFn = totalConditionValueMap[operator];
|
||||
/** 后端的 operator 枚举值通过 i18n 转化为文本 */
|
||||
const operatorData = isFunction(operatorFn) ? operatorFn() : operator;
|
||||
const leftData = left?.key ? { [left?.key]: left?.value } : left?.value;
|
||||
const rightData =
|
||||
right?.type === ConditionRightType.Ref && right?.key
|
||||
? { [right?.key]: right?.value }
|
||||
: right?.value;
|
||||
return {
|
||||
...condition,
|
||||
operatorData,
|
||||
leftData,
|
||||
rightData,
|
||||
};
|
||||
}),
|
||||
logic: branch.logic,
|
||||
logicData: logicTextMap.get(branch.logic),
|
||||
name: branch.name,
|
||||
}));
|
||||
logs.push({ conditions, type: LogType.Condition });
|
||||
} else {
|
||||
/** end、Message 节点的 label 不同,输入即输出 */
|
||||
const isOutputNode = type === 'End' || type === 'Message';
|
||||
const label = isOutputNode
|
||||
? I18n.t('workflow_detail_end_output')
|
||||
: I18n.t('workflow_detail_node_input');
|
||||
const copyTooltip = isOutputNode
|
||||
? I18n.t('workflow_detail_end_output_copy')
|
||||
: I18n.t('workflow_detail_title_testrun_copyinput');
|
||||
logs.push({
|
||||
label,
|
||||
data: inputData,
|
||||
copyTooltip,
|
||||
type: isOutputNode ? LogType.Output : LogType.Input,
|
||||
emptyPlaceholder: I18n.t(
|
||||
'workflow_testrun_input_form_empty',
|
||||
undefined,
|
||||
'本次试运行无需输入',
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generateOutput = (
|
||||
logs: Log[],
|
||||
result: NodeResult,
|
||||
node?: FlowNodeEntity,
|
||||
) => {
|
||||
const { NodeType: type, errorInfo, errorLevel, extra } = result;
|
||||
const responseExtra = (typeSafeJSONParse(extra) as any)?.response_extra || {};
|
||||
const { mock_hit_status: mockHitInfo } = responseExtra;
|
||||
|
||||
const { hitStatus: mockHitStatus, mockSetName } =
|
||||
(typeSafeJSONParse(mockHitInfo) as any) || {};
|
||||
/**
|
||||
* case1: output 解析成功,可能是对象或者字符串
|
||||
* case2: output 解析失败,可能是字符串
|
||||
* case3: output 为空值,兜底展示空对象
|
||||
*/
|
||||
let outputData =
|
||||
typeSafeJSONParse(result.output, { enableBigInt }) || result.output || {};
|
||||
/** step 2.1: 处理 rawOutput */
|
||||
/**
|
||||
* case1: output 解析成功,可能是对象或者字符串
|
||||
* case2: output 解析失败,可能是字符串,由于是原始输出空值也视为有意义
|
||||
*/
|
||||
const rawData =
|
||||
typeSafeJSONParse(result.raw_output, { enableBigInt }) || result.raw_output;
|
||||
|
||||
/** Code、Llm 节点需要展示 raw */
|
||||
const textHasRawout = type === 'Text' && hasStringOutput(rawData);
|
||||
const hasRawOutput =
|
||||
(type && ['Code', 'LLM', 'Question'].includes(type)) || textHasRawout;
|
||||
|
||||
/** step 2.2: 处理 errorInfo */
|
||||
if (errorInfo) {
|
||||
const errorData = {
|
||||
[errorLevel === 'Error'
|
||||
? LogObjSpecialKey.Error
|
||||
: LogObjSpecialKey.Warning]: errorInfo,
|
||||
};
|
||||
/**
|
||||
* 错误放到 output 中展示,output 的展示优先级最高
|
||||
* 若 output 为对象则直接 assign error
|
||||
* 否则 output 需要被赋值到 output 字段并和 error 组成一个对象
|
||||
*/
|
||||
outputData = isObject(outputData)
|
||||
? {
|
||||
...outputData,
|
||||
...errorData,
|
||||
}
|
||||
: { output: outputData, ...errorData };
|
||||
}
|
||||
|
||||
const finalOutputLog: OutputLog = {
|
||||
label: I18n.t('workflow_detail_node_output'),
|
||||
data: outputData,
|
||||
copyTooltip: I18n.t('workflow_detail_title_testrun_copyoutput'),
|
||||
nodeType: type || '',
|
||||
mockInfo: {
|
||||
isHit: mockHitStatus === MockHitStatus.Success,
|
||||
mockSetName,
|
||||
},
|
||||
rawOutput: hasRawOutput
|
||||
? {
|
||||
data: rawData,
|
||||
}
|
||||
: undefined,
|
||||
type: LogType.Output,
|
||||
};
|
||||
|
||||
if (type === 'LLM') {
|
||||
generateLLMOutput(logs, responseExtra, node);
|
||||
}
|
||||
|
||||
if (type === 'End') {
|
||||
const isReturnText =
|
||||
(typeSafeJSONParse((result as unknown as any).extra, {}) as any)
|
||||
?.response_extra?.terminal_plan === EndTerminalPlan.Text;
|
||||
if (isReturnText || errorInfo) {
|
||||
logs.push({
|
||||
...finalOutputLog,
|
||||
label: I18n.t('workflow_detail_end_answer'),
|
||||
copyTooltip: I18n.t('workflow_detail_end_answer_copy'),
|
||||
});
|
||||
}
|
||||
} else if (type === 'Message') {
|
||||
logs.push({
|
||||
...finalOutputLog,
|
||||
label: I18n.t('workflow_detail_end_answer'),
|
||||
copyTooltip: I18n.t('workflow_detail_end_answer_copy'),
|
||||
});
|
||||
} else if (type !== 'Start') {
|
||||
logs.push(finalOutputLog);
|
||||
}
|
||||
};
|
||||
|
||||
function getSubWorkflowId(node?: FlowNodeEntity) {
|
||||
const nodeData = node?.getData<WorkflowNodeData>(WorkflowNodeData);
|
||||
|
||||
if (!nodeData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return nodeData.getNodeData<StandardNodeType.SubWorkflow>()?.workflow_id;
|
||||
}
|
||||
|
||||
function generateExternalLink(
|
||||
logs: Log[],
|
||||
result: NodeResult,
|
||||
node?: FlowNodeEntity,
|
||||
) {
|
||||
const subWorkflowId = node ? getSubWorkflowId(node) : '';
|
||||
const { NodeType: type, executeId, subExecuteId } = result;
|
||||
if (type !== 'SubWorkflow') {
|
||||
return;
|
||||
}
|
||||
|
||||
logs.push({
|
||||
label: I18n.t('workflow_subwf_jump_detail'),
|
||||
type: LogType.WorkflowLink,
|
||||
data: {
|
||||
workflowId: subWorkflowId,
|
||||
executeId,
|
||||
subExecuteId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const generateLog = (
|
||||
result: NodeResult | null | undefined,
|
||||
node?: FlowNodeEntity,
|
||||
): {
|
||||
logs: Log[];
|
||||
} => {
|
||||
if (!result) {
|
||||
return { logs: [] };
|
||||
}
|
||||
const { isBatch } = result;
|
||||
|
||||
const logs: Log[] = [];
|
||||
|
||||
/** step 0: 处理 batch data */
|
||||
if (isBatch) {
|
||||
logs.push(generateBatchData(result));
|
||||
}
|
||||
|
||||
/** step 1: 处理 input */
|
||||
generateInput(logs, result);
|
||||
|
||||
/** step 2: 处理 output */
|
||||
|
||||
generateOutput(logs, result, node);
|
||||
|
||||
/** step 3: 对于子工作流节点,生成额外的跳转链接 */
|
||||
generateExternalLink(logs, result, node);
|
||||
|
||||
return {
|
||||
logs,
|
||||
};
|
||||
};
|
||||
@@ -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 { isOutputLog } from './field';
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { unified } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { isString } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 是否符合渲染为 markdown
|
||||
* 1. ast > 1 或
|
||||
* 2. ast = 1 且类型不为普通段落
|
||||
* 2. ast = 1 且类型为普通段落,但段落中超过两个或者仅有一项但不为 text
|
||||
*/
|
||||
export const isPreviewMarkdown = (str: unknown) => {
|
||||
if (!isString(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tree = unified().use(remarkParse).parse(str);
|
||||
|
||||
if (tree.children.length > 1) {
|
||||
return true;
|
||||
}
|
||||
if (tree.children.length === 1) {
|
||||
const [child] = tree.children;
|
||||
if (child.type !== 'paragraph') {
|
||||
return true;
|
||||
} else if (
|
||||
child.children.length > 1 ||
|
||||
child.children?.[0]?.type !== 'text'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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 { get, isString, omit } from 'lodash-es';
|
||||
import JSONBig from 'json-bigint';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowNodeData,
|
||||
LLMNodeDataSkillType,
|
||||
type LLMNodeDataSkill,
|
||||
type LLMNodeDataDatasetSkill,
|
||||
type LLMNodeDataPluginSkill,
|
||||
type LLMNodeDataWorkflowSkill,
|
||||
} from '@coze-workflow/nodes';
|
||||
import { type StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type FunctionCallLog, type FunctionCallLogItem } from '../types';
|
||||
import { LogType } from '../constants';
|
||||
|
||||
interface FunctionCallDetailItem {
|
||||
input: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
interface FunctionCallDetailInput {
|
||||
name?: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
plugin_id?: number;
|
||||
plugin_name?: string;
|
||||
api_id?: number;
|
||||
api_name?: string;
|
||||
plugin_type?: number;
|
||||
}
|
||||
|
||||
interface FunctionCallKnowledgeOutput {
|
||||
chunks: {
|
||||
slice: string;
|
||||
score: number;
|
||||
meta: {
|
||||
dataset: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
interface FunctionCallDetailOutput {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface FunctionCallDetail {
|
||||
fc_called_list: FunctionCallDetailItem[];
|
||||
}
|
||||
|
||||
function getNodeSkills(node?: FlowNodeEntity): LLMNodeDataSkill[] {
|
||||
const nodeData = node?.getData(WorkflowNodeData);
|
||||
|
||||
if (!nodeData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodeData.getNodeData<StandardNodeType.LLM>()?.skills || [];
|
||||
}
|
||||
|
||||
function getPluginLogItem(
|
||||
input: FunctionCallDetailInput,
|
||||
output: FunctionCallDetailOutput | string,
|
||||
skills: LLMNodeDataSkill[],
|
||||
): FunctionCallLogItem {
|
||||
const nodeSkill = skills.find(
|
||||
skill =>
|
||||
skill.type === LLMNodeDataSkillType.Plugin &&
|
||||
`${skill.apiId}` === `${input.api_id}`,
|
||||
) as LLMNodeDataPluginSkill;
|
||||
|
||||
const pluginName = nodeSkill?.pluginName || input.plugin_name || '';
|
||||
const apiName = input.api_name;
|
||||
|
||||
const logItem: FunctionCallLogItem = {
|
||||
name: `${pluginName} - ${apiName}`,
|
||||
inputs: input.arguments || {},
|
||||
outputs: output || {},
|
||||
icon: nodeSkill?.icon || '',
|
||||
};
|
||||
return logItem;
|
||||
}
|
||||
|
||||
function getWorkflowLogItem(
|
||||
input: FunctionCallDetailInput,
|
||||
output: FunctionCallDetailOutput | string,
|
||||
skills: LLMNodeDataSkill[],
|
||||
): FunctionCallLogItem {
|
||||
const nodeSkill = skills.find(
|
||||
skill =>
|
||||
skill.type === LLMNodeDataSkillType.Workflow &&
|
||||
`${skill.pluginId}` === `${input.plugin_id}`,
|
||||
) as LLMNodeDataWorkflowSkill;
|
||||
|
||||
const logItem: FunctionCallLogItem = {
|
||||
name: nodeSkill?.name || '',
|
||||
inputs: input?.arguments || {},
|
||||
outputs: isString(output)
|
||||
? output
|
||||
: (output as Record<string, unknown>) || {},
|
||||
icon: nodeSkill?.icon || '',
|
||||
};
|
||||
return logItem;
|
||||
}
|
||||
|
||||
function getDatasetLogItem(
|
||||
output: FunctionCallKnowledgeOutput,
|
||||
skills: LLMNodeDataSkill[],
|
||||
): FunctionCallLogItem | null {
|
||||
const chunk = get(output, 'chunks[0]');
|
||||
if (!chunk) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const oriReq = get(output, 'ori_req');
|
||||
let inputs;
|
||||
|
||||
if (oriReq) {
|
||||
try {
|
||||
inputs = JSON.parse(oriReq);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const id = get(chunk, 'meta.dataset.id');
|
||||
|
||||
const nodeSkill = skills.find(
|
||||
skill =>
|
||||
skill.type === LLMNodeDataSkillType.Dataset && `${skill.id}` === `${id}`,
|
||||
) as LLMNodeDataDatasetSkill;
|
||||
|
||||
const name = nodeSkill?.name || get(chunk, 'dataset.name') || '';
|
||||
|
||||
const logItem: FunctionCallLogItem = {
|
||||
name,
|
||||
outputs: omit(chunk, 'meta'),
|
||||
inputs,
|
||||
icon: nodeSkill?.icon || '',
|
||||
};
|
||||
return logItem;
|
||||
}
|
||||
|
||||
function parseOutput(
|
||||
jsonBig: JSONBig,
|
||||
output: string,
|
||||
): FunctionCallDetailOutput | string {
|
||||
try {
|
||||
return jsonBig.parse(output) as FunctionCallDetailOutput;
|
||||
// eslint-disable-next-line @coze-arch/no-empty-catch, @coze-arch/use-error-in-catch, no-empty
|
||||
} catch (e) {}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function parseFunctionCall(
|
||||
fcCalledDetail: FunctionCallDetail,
|
||||
node?: FlowNodeEntity,
|
||||
): FunctionCallLog {
|
||||
let items: FunctionCallLogItem[] = [];
|
||||
const jsonBig = JSONBig({ storeAsString: true });
|
||||
|
||||
items = (fcCalledDetail?.fc_called_list || [])
|
||||
.map(detailItem => {
|
||||
try {
|
||||
const input = detailItem.input
|
||||
? (jsonBig.parse(detailItem.input) as FunctionCallDetailInput)
|
||||
: {};
|
||||
const output = detailItem.output
|
||||
? (parseOutput(jsonBig, detailItem.output) as
|
||||
| FunctionCallDetailOutput
|
||||
| string)
|
||||
: {};
|
||||
|
||||
const type = get(output, 'msg_type');
|
||||
const skills = getNodeSkills(node);
|
||||
|
||||
// 知识库类型
|
||||
if (type === 'knowledge_recall') {
|
||||
const data: FunctionCallKnowledgeOutput = JSON.parse(
|
||||
get(output, 'data', '{}') as string,
|
||||
);
|
||||
return getDatasetLogItem(data, skills);
|
||||
}
|
||||
|
||||
// 插件类型
|
||||
if (input?.plugin_type === LLMNodeDataSkillType.Plugin) {
|
||||
return getPluginLogItem(input, output, skills);
|
||||
}
|
||||
|
||||
// Workflow类型
|
||||
if (input?.plugin_type === LLMNodeDataSkillType.Workflow) {
|
||||
return getWorkflowLogItem(input, output, skills);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as FunctionCallLogItem[];
|
||||
|
||||
return {
|
||||
type: LogType.FunctionCall,
|
||||
items,
|
||||
data: items,
|
||||
copyTooltip: I18n.t('workflow_250310_13', undefined, '复制'),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.group-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -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 css from './base-group-wrap.module.less';
|
||||
|
||||
interface BaseGroupWrapProps {
|
||||
title?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BaseGroupWrap: React.FC<
|
||||
React.PropsWithChildren<BaseGroupWrapProps>
|
||||
> = ({ title, children }) => (
|
||||
<div className={css['group-wrap']}>
|
||||
{title ? <div className={css.title}>{title}</div> : null}
|
||||
{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 { ProblemGroup } from './problem-group';
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 { Tag, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { NodeItem, LineItem, TextItem } from '../problem-item';
|
||||
import { type WorkflowProblem, type ProblemItem } from '../../types';
|
||||
import { BaseGroupWrap } from './base-group-wrap';
|
||||
|
||||
interface MyProblemGroupProps {
|
||||
problems: WorkflowProblem;
|
||||
isMine?: boolean;
|
||||
showTitle?: boolean;
|
||||
onClick: (p: ProblemItem) => void;
|
||||
}
|
||||
|
||||
export const MyProblemGroup: React.FC<MyProblemGroupProps> = ({
|
||||
problems,
|
||||
isMine,
|
||||
showTitle,
|
||||
onClick,
|
||||
}) => {
|
||||
const { node, line } = problems.problems;
|
||||
return (
|
||||
<BaseGroupWrap
|
||||
title={
|
||||
showTitle ? (
|
||||
<>
|
||||
<Typography.Text
|
||||
strong
|
||||
fontSize="14px"
|
||||
className="coz-fg-secondary"
|
||||
>
|
||||
{problems.name}
|
||||
</Typography.Text>
|
||||
|
||||
<Tag className="ml-2" size="small" color="primary">
|
||||
{isMine
|
||||
? I18n.t('wf_problem_my_tag')
|
||||
: I18n.t('wf_problem_other_tag')}
|
||||
</Tag>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{node.map(i =>
|
||||
isMine ? (
|
||||
<NodeItem problem={i} onClick={onClick} />
|
||||
) : (
|
||||
<TextItem problem={i} onClick={onClick} />
|
||||
),
|
||||
)}
|
||||
{line.map((i, idx) =>
|
||||
isMine ? (
|
||||
<LineItem problem={i} idx={idx} onClick={onClick} />
|
||||
) : (
|
||||
<TextItem problem={i} onClick={onClick} />
|
||||
),
|
||||
)}
|
||||
</BaseGroupWrap>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.problem-group {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -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 { ProblemEmpty } from '../problem-panel/empty';
|
||||
import { type WorkflowProblem, type ProblemItem } from '../../types';
|
||||
import { MyProblemGroup } from './my-problem-group';
|
||||
|
||||
import css from './problem-group.module.less';
|
||||
|
||||
interface ProblemGroupProps {
|
||||
myProblems?: WorkflowProblem;
|
||||
otherProblems: WorkflowProblem[];
|
||||
onScroll: (p: ProblemItem) => void;
|
||||
onJump: (p: ProblemItem, workflowId: string) => void;
|
||||
}
|
||||
|
||||
export const ProblemGroup: React.FC<ProblemGroupProps> = ({
|
||||
myProblems,
|
||||
otherProblems,
|
||||
onScroll,
|
||||
onJump,
|
||||
}) => {
|
||||
const isEmpty = !myProblems && !otherProblems.length;
|
||||
|
||||
if (isEmpty) {
|
||||
return <ProblemEmpty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css['problem-group']}>
|
||||
{myProblems ? (
|
||||
<MyProblemGroup
|
||||
problems={myProblems}
|
||||
showTitle={!!otherProblems.length}
|
||||
isMine
|
||||
onClick={onScroll}
|
||||
/>
|
||||
) : null}
|
||||
{otherProblems.map(other => (
|
||||
<MyProblemGroup
|
||||
problems={other}
|
||||
showTitle={true}
|
||||
onClick={p => onJump(p, other.workflowId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
.base-item {
|
||||
display: flex;
|
||||
column-gap: 12px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
.text-item {
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
}
|
||||
.item-popover {
|
||||
font-size: 12px;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
.item-info {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.base-item-wrap {
|
||||
border: 0.5px solid var(--coz-stroke-primary);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--coz-shadow-small);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgba(77, 77, 77, 0.05);
|
||||
}
|
||||
&:active {
|
||||
background: rgba(77, 77, 77, 0.15);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Popover, Typography, Tag } from '@coze-arch/coze-design';
|
||||
|
||||
import { type ProblemItem } from '../../types';
|
||||
|
||||
import styles from './base-item.module.less';
|
||||
|
||||
type BaseItemWrapProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const BaseItemWrap: React.FC<
|
||||
React.PropsWithChildren<BaseItemWrapProps>
|
||||
> = ({ className, ...props }) => (
|
||||
<div className={cls(styles['base-item-wrap'], className)} {...props}></div>
|
||||
);
|
||||
|
||||
interface BaseItemProps {
|
||||
problem: ProblemItem;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
popover?: React.ReactNode;
|
||||
onClick: (p: ProblemItem) => void;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const BaseItem: React.FC<BaseItemProps> = ({
|
||||
problem,
|
||||
title,
|
||||
icon,
|
||||
popover,
|
||||
onClick,
|
||||
}) => {
|
||||
const { errorInfo, errorLevel } = problem;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(problem);
|
||||
}, [problem, onClick]);
|
||||
|
||||
return (
|
||||
<BaseItemWrap className={styles['base-item']} onClick={handleClick}>
|
||||
<div className={styles['item-icon']}>{icon}</div>
|
||||
<div className={styles['item-content']}>
|
||||
<div className={styles['item-title']}>
|
||||
<Text weight={500}>{title}</Text>
|
||||
{errorLevel === 'warning' && (
|
||||
<Tag color="primary">{I18n.t('workflow_exception_ignore_tag')}</Tag>
|
||||
)}
|
||||
{popover ? (
|
||||
<Popover content={popover} position="top">
|
||||
<IconCozInfoCircle className={styles['item-popover']} />
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles['item-info']}>
|
||||
<Text
|
||||
size="small"
|
||||
className={
|
||||
errorLevel === 'error' ? 'coz-fg-hglt-red' : 'coz-fg-hglt-yellow'
|
||||
}
|
||||
>
|
||||
{errorInfo}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</BaseItemWrap>
|
||||
);
|
||||
};
|
||||
|
||||
export const TextItem: React.FC<{
|
||||
problem: ProblemItem;
|
||||
onClick: (p: ProblemItem) => void;
|
||||
}> = ({ problem, onClick }) => {
|
||||
const { errorInfo, errorLevel } = problem;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(problem);
|
||||
}, [problem, onClick]);
|
||||
|
||||
return (
|
||||
<BaseItemWrap className={styles['text-item']} onClick={handleClick}>
|
||||
<Text
|
||||
size="small"
|
||||
className={
|
||||
errorLevel === 'error' ? 'coz-fg-hglt-red' : 'coz-fg-hglt-yellow'
|
||||
}
|
||||
>
|
||||
{errorInfo}
|
||||
</Text>
|
||||
</BaseItemWrap>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { NodeItem } from './node-item';
|
||||
export { LineItem } from './line-item';
|
||||
export { TextItem } from './base-item';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,12 @@
|
||||
.line-icon {
|
||||
font-size: 32px;
|
||||
color: var(--coz-fg-hglt-red);
|
||||
}
|
||||
|
||||
.line-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
img {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozAddNode } from '@coze-arch/coze-design/icons';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { type ProblemItem } from '../../types';
|
||||
import i18n from './line-case-i18n.png';
|
||||
import cn from './line-case-cn.png';
|
||||
import { BaseItem } from './base-item';
|
||||
|
||||
import styles from './line-item.module.less';
|
||||
|
||||
interface LineItemProps {
|
||||
problem: ProblemItem;
|
||||
idx: number;
|
||||
onClick: (p: ProblemItem) => void;
|
||||
}
|
||||
|
||||
const LinePopover = () => {
|
||||
const lang = I18n.getLanguages();
|
||||
const currentLang = lang[0];
|
||||
|
||||
return (
|
||||
<div className={styles['line-popover']}>
|
||||
<Typography.Text fontSize="16px">
|
||||
{I18n.t('workflow_running_results_line_error')}
|
||||
</Typography.Text>
|
||||
<img src={['zh-CN', 'zh'].includes(currentLang) ? cn : i18n} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LineItem: React.FC<LineItemProps> = ({
|
||||
problem,
|
||||
idx,
|
||||
onClick,
|
||||
}) => (
|
||||
<BaseItem
|
||||
problem={problem}
|
||||
title={`${I18n.t('workflow_connection_name')}${idx + 1}`}
|
||||
icon={<IconCozAddNode className={styles['line-icon']} />}
|
||||
popover={<LinePopover />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
@@ -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 { useEffect, useState } from 'react';
|
||||
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { usePlayground } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowNodeData,
|
||||
type CommonNodeData,
|
||||
type NodeData,
|
||||
} from '@coze-workflow/nodes';
|
||||
import { Avatar } from '@coze-arch/coze-design';
|
||||
|
||||
import { type ProblemItem } from '../../types';
|
||||
import { BaseItem } from './base-item';
|
||||
|
||||
interface NodeItemProps {
|
||||
problem: ProblemItem;
|
||||
onClick: (p: ProblemItem) => void;
|
||||
}
|
||||
|
||||
// 避免节点删除后丢失icon、title信息
|
||||
const useMetaMemo = (nodeId: string) => {
|
||||
const [nodeMeta, setNodeMeta] = useState<CommonNodeData>();
|
||||
const playground = usePlayground();
|
||||
|
||||
const node = playground.entityManager.getEntityById<FlowNodeEntity>(nodeId);
|
||||
const nodeData = node?.getData<WorkflowNodeData>(WorkflowNodeData);
|
||||
const meta = nodeData?.getNodeData<keyof NodeData>();
|
||||
|
||||
useEffect(() => {
|
||||
if (meta && !isEqual(nodeMeta, meta)) {
|
||||
setNodeMeta(meta);
|
||||
}
|
||||
}, [meta]);
|
||||
|
||||
return nodeMeta;
|
||||
};
|
||||
|
||||
export const NodeItem: React.FC<NodeItemProps> = ({ problem, onClick }) => {
|
||||
const meta = useMetaMemo(problem.nodeId);
|
||||
|
||||
return (
|
||||
<BaseItem
|
||||
problem={problem}
|
||||
title={meta?.title || ''}
|
||||
icon={<Avatar src={meta?.icon} shape="square" size="small" />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.problem-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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 { IconCozIllusDone } from '@coze-arch/coze-design/illustrations';
|
||||
|
||||
import styles from './empty.module.less';
|
||||
|
||||
export const ProblemEmpty = () => (
|
||||
<div className={styles['problem-empty']}>
|
||||
<IconCozIllusDone width="120" height="120" />
|
||||
</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 { ProblemPanel } from './problem-panel';
|
||||
@@ -0,0 +1,32 @@
|
||||
.problem-list {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
padding: 12px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.base-panel {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
|
||||
.checking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--coz-fg-secondary);
|
||||
column-gap: 2px;
|
||||
|
||||
:global(.semi-spin-wrapper) {
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
import { ProblemGroup } from '../problem-group';
|
||||
import { type ProblemItem } from '../../types';
|
||||
import { useProblems } from '../../hooks/use-problems';
|
||||
import { BasePanel } from '../../../../components';
|
||||
|
||||
import styles from './problem-panel.module.less';
|
||||
|
||||
interface ProblemPanelProps {
|
||||
maxHeight: number;
|
||||
workflowId: string;
|
||||
onScroll: (p: ProblemItem) => void;
|
||||
onJump: (p: ProblemItem, workflowId: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ProblemPanel: React.FC<ProblemPanelProps> = ({
|
||||
maxHeight,
|
||||
workflowId,
|
||||
onScroll,
|
||||
onJump,
|
||||
onClose,
|
||||
}) => {
|
||||
const { problemsV2, validating } = useProblems(workflowId);
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
header={
|
||||
<div className={styles['panel-title']}>
|
||||
{I18n.t('card_builder_check_title')}
|
||||
{validating ? (
|
||||
<div className={styles.checking}>
|
||||
<Spin size="small" />
|
||||
{I18n.t('wf_testrun_problems_loading')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
height={300}
|
||||
resizable={{
|
||||
min: 300,
|
||||
max: maxHeight,
|
||||
}}
|
||||
onClose={onClose}
|
||||
className={styles['base-panel']}
|
||||
>
|
||||
<ProblemGroup {...problemsV2} onScroll={onScroll} onJump={onJump} />
|
||||
</BasePanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 { uniq } from 'lodash-es';
|
||||
import {
|
||||
useValidationServiceStore,
|
||||
type ValidateError,
|
||||
type ValidationState,
|
||||
} from '@coze-workflow/base/services';
|
||||
|
||||
import { type WorkflowProblem } from '../types';
|
||||
|
||||
const generateErrors2Problems = (errors: ValidationState['errors']) => {
|
||||
const nodeProblems: ValidateError[] = [];
|
||||
const lineProblems: ValidateError[] = [];
|
||||
Object.entries(errors).forEach(([id, list]) => {
|
||||
const nodeErrors = list.filter(i => i.errorType === 'node');
|
||||
const lineErrors = list.filter(i => i.errorType === 'line');
|
||||
|
||||
// 处理节点错误
|
||||
const nodeLevelErrors = nodeErrors.filter(
|
||||
item => item.errorLevel === 'error',
|
||||
);
|
||||
const nodeLevelWarnings = nodeErrors.filter(
|
||||
item => item.errorLevel === 'warning',
|
||||
);
|
||||
// errors 优先,其次才显示 warning
|
||||
const nodeCurrentErrors = nodeLevelErrors.length
|
||||
? nodeLevelErrors
|
||||
: nodeLevelWarnings;
|
||||
if (nodeCurrentErrors.length) {
|
||||
const nodeProblem: ValidateError = {
|
||||
nodeId: id,
|
||||
errorInfo: uniq(nodeCurrentErrors.map(error => error.errorInfo)).join(
|
||||
';',
|
||||
),
|
||||
errorLevel: nodeCurrentErrors[0].errorLevel,
|
||||
errorType: 'node',
|
||||
};
|
||||
nodeProblems.push(nodeProblem);
|
||||
}
|
||||
|
||||
// 处理线错误
|
||||
if (lineErrors.length) {
|
||||
lineProblems.push(...lineErrors);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
node: nodeProblems,
|
||||
line: lineProblems,
|
||||
};
|
||||
};
|
||||
|
||||
const generateProblemsV2 = (
|
||||
errors: ValidationState['errorsV2'],
|
||||
workflowId: string,
|
||||
) => {
|
||||
let myProblems: WorkflowProblem | undefined;
|
||||
const otherProblems: WorkflowProblem[] = [];
|
||||
Object.entries(errors).forEach(([id, error]) => {
|
||||
if (!Object.keys(error.errors).length) {
|
||||
return;
|
||||
}
|
||||
const value = {
|
||||
...error,
|
||||
problems: generateErrors2Problems(error.errors),
|
||||
};
|
||||
if (id === workflowId) {
|
||||
myProblems = value;
|
||||
} else {
|
||||
otherProblems.push(value);
|
||||
}
|
||||
});
|
||||
return {
|
||||
myProblems,
|
||||
otherProblems,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProblems = (workflowId: string) => {
|
||||
const { errorsV2, validating } = useValidationServiceStore(store => ({
|
||||
errorsV2: store.errorsV2,
|
||||
validating: store.validating,
|
||||
}));
|
||||
|
||||
const problemsV2 = useMemo(
|
||||
() => generateProblemsV2(errorsV2, workflowId),
|
||||
[errorsV2, workflowId],
|
||||
);
|
||||
|
||||
return { problemsV2, validating };
|
||||
};
|
||||
@@ -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 { ProblemPanel } from './components/problem-panel';
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 FeedbackStatus } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type WorkflowValidateError } from '@coze-workflow/base/services';
|
||||
export type WorkflowProblem = WorkflowValidateError & {
|
||||
problems: {
|
||||
node: ProblemItem[];
|
||||
line: ProblemItem[];
|
||||
};
|
||||
};
|
||||
|
||||
export interface ProblemItem {
|
||||
// 错误描述
|
||||
errorInfo: string;
|
||||
// 错误等级
|
||||
errorLevel: FeedbackStatus;
|
||||
// 错误类型: 节点 / 连线
|
||||
errorType: 'node' | 'line';
|
||||
// 节点id
|
||||
nodeId: string;
|
||||
// 若为连线错误,还需要目标节点来确认这条连线
|
||||
targetNodeId?: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.answer-input {
|
||||
padding: 0 12px 16px 12px;
|
||||
}
|
||||
@@ -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 { useState } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozSendFill } from '@coze-arch/coze-design/icons';
|
||||
import { Input, IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { useSendMessage } from '../../hooks';
|
||||
|
||||
import styles from './answer-input.module.less';
|
||||
|
||||
export const AnswerInput = () => {
|
||||
const { send, waiting } = useSendMessage();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const disabled = value === '' || waiting;
|
||||
|
||||
const handleSend = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
send(value);
|
||||
|
||||
setValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles['answer-input']}>
|
||||
<Input
|
||||
placeholder={I18n.t(
|
||||
'workflow_ques_ans_testrun_message_placeholder',
|
||||
{},
|
||||
'Send a message',
|
||||
)}
|
||||
value={value}
|
||||
onChange={val => setValue(val)}
|
||||
onEnterPress={handleSend}
|
||||
autoFocus
|
||||
size="large"
|
||||
suffix={
|
||||
<IconButton
|
||||
icon={<IconCozSendFill />}
|
||||
disabled={disabled}
|
||||
onClick={handleSend}
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -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 { MessageList } from './message-list';
|
||||
@@ -0,0 +1,18 @@
|
||||
.message-item {
|
||||
display: flex;
|
||||
column-gap: 12px;
|
||||
}
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
}
|
||||
.message-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.user-name {
|
||||
font-size: 12px;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { Avatar } from '@coze-arch/coze-design';
|
||||
|
||||
import { type ReceivedMessage } from '../../types';
|
||||
import { ContentType, MessageType } from '../../constants';
|
||||
import userAvatar from './user-avatar.png';
|
||||
import { TextMessage } from './text-message';
|
||||
import { OptionMessage } from './option-message';
|
||||
import { MessageLoading } from './message-loading';
|
||||
import botAvatar from './bot-avatar.png';
|
||||
|
||||
import styles from './message-item.module.less';
|
||||
|
||||
const BotAvatar = () => (
|
||||
<Avatar src={botAvatar} size="small" className={styles['message-avatar']} />
|
||||
);
|
||||
const UserAvatar = () => (
|
||||
<Avatar src={userAvatar} size="small" className={styles['message-avatar']} />
|
||||
);
|
||||
|
||||
interface MessageItemProps {
|
||||
message: ReceivedMessage;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const MessageItem: React.FC<MessageItemProps> = ({
|
||||
message,
|
||||
loading,
|
||||
}) => {
|
||||
const { content_type: contentType, type } = message;
|
||||
|
||||
return (
|
||||
<div className={styles['message-item']}>
|
||||
{type === MessageType.Question ? <BotAvatar /> : <UserAvatar />}
|
||||
|
||||
<div className={styles['message-main']}>
|
||||
<div className={styles['user-name']}>
|
||||
{type === MessageType.Question
|
||||
? I18n.t('workflow_ques_ans_testrun_botname', {}, 'Bot')
|
||||
: I18n.t('workflow_ques_ans_testrun_username', {}, 'User')}
|
||||
</div>
|
||||
{loading ? <MessageLoading /> : null}
|
||||
{!loading && contentType === ContentType.Option && (
|
||||
<OptionMessage message={message} />
|
||||
)}
|
||||
{!loading && contentType === ContentType.Text && (
|
||||
<TextMessage message={message} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user