feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { AddButton } from '@/form';
|
||||
|
||||
export interface AddOptionButtonProps {
|
||||
/** 是否展示标题行 */
|
||||
showTitleRow?: boolean;
|
||||
|
||||
/** 是否展示选项标签 */
|
||||
showOptionName?: boolean;
|
||||
|
||||
/** 选项 placeholder */
|
||||
optionPlaceholder?: string;
|
||||
|
||||
/** 默认分支名称 */
|
||||
defaultOptionText?: string;
|
||||
|
||||
/** 选项最大数量限制,默认值为整数最大值 */
|
||||
maxItems?: number;
|
||||
|
||||
/** 展示禁止添加 Tooltip */
|
||||
showDisableAddTooltip?: boolean;
|
||||
customDisabledAddTooltip?: string;
|
||||
className?: string;
|
||||
dataTestId?: string;
|
||||
value;
|
||||
onClick;
|
||||
readonly;
|
||||
children;
|
||||
}
|
||||
|
||||
export const AddOptionButton = ({
|
||||
className,
|
||||
showDisableAddTooltip = true,
|
||||
maxItems = Number.MAX_SAFE_INTEGER,
|
||||
customDisabledAddTooltip,
|
||||
value,
|
||||
onClick,
|
||||
readonly,
|
||||
children,
|
||||
dataTestId,
|
||||
}: AddOptionButtonProps) =>
|
||||
showDisableAddTooltip && (value?.length as number) >= maxItems ? (
|
||||
<Tooltip
|
||||
content={
|
||||
customDisabledAddTooltip ||
|
||||
I18n.t('workflow_250117_05', { maxCount: maxItems })
|
||||
}
|
||||
>
|
||||
<AddButton
|
||||
className={className}
|
||||
children={children}
|
||||
dataTestId={dataTestId}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<AddButton
|
||||
className={className}
|
||||
disabled={readonly}
|
||||
children={children}
|
||||
onClick={onClick}
|
||||
dataTestId={dataTestId}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
|
||||
import { ExpressionEditor } from '@/nodes-v2/components/expression-editor';
|
||||
import {
|
||||
calcPortId,
|
||||
convertNumberToLetters,
|
||||
} from '@/form-extensions/setters/answer-option/utils';
|
||||
import { withField, useField, SortableItem, FieldArrayItem } from '@/form';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const AnswerItemField = withField(
|
||||
({
|
||||
showOptionName,
|
||||
optionPlaceholder,
|
||||
optionIndex,
|
||||
movePostion,
|
||||
onItemDelete,
|
||||
answerOptions,
|
||||
}: {
|
||||
showOptionName?: boolean;
|
||||
optionPlaceholder?: string;
|
||||
optionIndex: number;
|
||||
movePostion: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
onItemDelete: (index: number) => void;
|
||||
answerOptions: { name: string }[];
|
||||
}) => {
|
||||
const { value, onChange, onBlur, readonly, errors } = useField<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
movePostion?.start === optionIndex ||
|
||||
movePostion?.end === optionIndex
|
||||
) {
|
||||
onBlur?.();
|
||||
}
|
||||
}, [movePostion]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<SortableItem
|
||||
key={optionIndex}
|
||||
sortableID={calcPortId(optionIndex)}
|
||||
index={optionIndex}
|
||||
containerClassName="items-center"
|
||||
hanlderClassName="!p-0"
|
||||
>
|
||||
<FieldArrayItem
|
||||
className="!pt-[0px] items-center"
|
||||
containerClassName="items-center"
|
||||
removeIconClassName="!h-[24px] !min-w-[24px] !max-w-[24px] !p-[4px]"
|
||||
disableRemove={answerOptions?.length <= 1 || readonly}
|
||||
onRemove={() => {
|
||||
if (answerOptions?.length <= 1 || readonly) {
|
||||
return;
|
||||
}
|
||||
onItemDelete(optionIndex);
|
||||
}}
|
||||
>
|
||||
{showOptionName ? (
|
||||
<div className="break-keep w-[48px]">
|
||||
{convertNumberToLetters(optionIndex)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="items-center space-x-1 w-full min-h-[24px] leading-[24px]">
|
||||
{!readonly ? (
|
||||
<ExpressionEditor
|
||||
name={'/questionParams/options'}
|
||||
value={value as string}
|
||||
onChangeTrigger="onChange"
|
||||
onChange={val => onChange?.(val as string)}
|
||||
onBlur={() => {
|
||||
onBlur?.();
|
||||
}}
|
||||
isError={errors && errors?.length > 0}
|
||||
minRows={1}
|
||||
placeholder={optionPlaceholder}
|
||||
disableSuggestion={false}
|
||||
className="!px-[4px] !py-[2px]"
|
||||
containerClassName={styles['answer-editor']}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full">{value ?? ''}</div>
|
||||
)}
|
||||
</div>
|
||||
</FieldArrayItem>
|
||||
</SortableItem>
|
||||
<FormItemFeedback className="pl-[68px]" errors={errors} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
.parameters-title {
|
||||
width: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.parameters-title-readonly {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.parameters-wrapper {
|
||||
background: var(--Bg-COZ-bg-max, #FFF);
|
||||
border: 1px solid var(--Stroke-COZ-stroke-plus, rgba(84, 97, 156, 27%));
|
||||
border-radius: var(--small, 6px);
|
||||
|
||||
:global {
|
||||
.semi-radio-content{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.answer-editor{
|
||||
:global{
|
||||
.cm-editor .cm-scroller{
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-radio-group {
|
||||
width: 50%;
|
||||
|
||||
:global{
|
||||
.semi-radio-content span{
|
||||
color: var(--Fg-COZ-fg-primary, rgba(15, 21, 40, 82%));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import update from 'immutability-helper';
|
||||
import classNames from 'classnames';
|
||||
import { ViewVariableType } from '@coze-workflow/variable';
|
||||
import { useNodeTestId } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { ValueExpressionInputField } from '@/node-registries/common/fields';
|
||||
import { useUpdateSortedPortLines } from '@/hooks';
|
||||
import { ColumnsTitleWithAction } from '@/form-extensions/components/columns-title-with-action';
|
||||
import { RadioGroupField } from '@/form/fields';
|
||||
import {
|
||||
Section,
|
||||
useFieldArray,
|
||||
SortableList,
|
||||
FieldArray,
|
||||
useWatch,
|
||||
} from '@/form';
|
||||
import { OptionType } from '@/constants/question-settings';
|
||||
|
||||
import { generatePortId } from './utils';
|
||||
import { AnswerItemField } from './answer-item';
|
||||
import { AddOptionButton } from './add-option-button';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface AnswerOptionProps {
|
||||
/** 是否展示标题行 */
|
||||
showTitleRow?: boolean;
|
||||
|
||||
/** 是否展示选项标签 */
|
||||
showOptionName?: boolean;
|
||||
|
||||
/** 选项 placeholder */
|
||||
optionPlaceholder?: string;
|
||||
|
||||
/** 默认分支名称 */
|
||||
defaultOptionText?: string;
|
||||
}
|
||||
|
||||
const AnswerOption = ({
|
||||
optionPlaceholder,
|
||||
defaultOptionText,
|
||||
showTitleRow = true,
|
||||
showOptionName = true,
|
||||
}: AnswerOptionProps) => {
|
||||
const { value, move, name, readonly, onChange, append } = useFieldArray<{
|
||||
name: string;
|
||||
}>();
|
||||
const answerType = useWatch({ name: 'questionParams.answer_type' });
|
||||
const optionType = useWatch({ name: 'questionParams.option_type' });
|
||||
|
||||
const { getNodeSetterId } = useNodeTestId();
|
||||
const updateSortedPortLines = useUpdateSortedPortLines(generatePortId);
|
||||
const [movePostion, setMovePostion] = useState<{
|
||||
start?: number;
|
||||
end?: number;
|
||||
}>({});
|
||||
|
||||
const isStaticOption = useMemo(
|
||||
() => optionType === OptionType.Static,
|
||||
[optionType],
|
||||
);
|
||||
|
||||
if (answerType !== 'option') {
|
||||
return;
|
||||
}
|
||||
|
||||
const onItemDelete = (index: number) => {
|
||||
// 将要被删除的端口移动到最后,这样删除时不会对其他连线顺序产生影响
|
||||
updateSortedPortLines(index, value?.length as number);
|
||||
|
||||
const newVal = update(value, { $splice: [[index, 1]] });
|
||||
onChange(newVal);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section
|
||||
title={
|
||||
<div className="text-xs font-normal">
|
||||
{I18n.t('workflow_ques_ans_type_option_label', {}, '设置选项内容')}
|
||||
</div>
|
||||
}
|
||||
headerClassName="pt-[12px] !mb-[4px]"
|
||||
noPadding
|
||||
collapsible={false}
|
||||
>
|
||||
<div className={`w-full p-[8px] ${styles['parameters-wrapper']}`}>
|
||||
<div className="w-full mb-[8px]">
|
||||
<RadioGroupField
|
||||
name="questionParams.option_type"
|
||||
data-testid={getNodeSetterId('questionParams.option_type')}
|
||||
type="button"
|
||||
className="w-full"
|
||||
buttonSize="middle"
|
||||
defaultValue={OptionType.Static}
|
||||
options={[
|
||||
{
|
||||
label: I18n.t(
|
||||
'workflow_question_fixed_content',
|
||||
{},
|
||||
'固定内容',
|
||||
),
|
||||
value: OptionType.Static,
|
||||
className: styles['option-radio-group'],
|
||||
},
|
||||
{
|
||||
label: I18n.t(
|
||||
'workflow_question_ dynamic_content',
|
||||
{},
|
||||
'动态内容',
|
||||
),
|
||||
value: OptionType.Dynamic,
|
||||
className: styles['option-radio-group'],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{showTitleRow ? (
|
||||
<ColumnsTitleWithAction
|
||||
columns={[
|
||||
{
|
||||
title: I18n.t('workflow_ques_ans_type_option_title'),
|
||||
style: {
|
||||
width: isStaticOption ? '67px' : '56px',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: I18n.t('workflow_ques_ans_type_option_content'),
|
||||
style: {
|
||||
flex: 1,
|
||||
},
|
||||
},
|
||||
]}
|
||||
readonly={readonly}
|
||||
className={classNames(
|
||||
'mb-[8px]',
|
||||
readonly
|
||||
? styles.parametersTitleReadonly
|
||||
: styles.parametersTitle,
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{!isStaticOption ? (
|
||||
<div className="flex items-center w-full text-xs mt-2">
|
||||
<div
|
||||
className="break-keep mr-[4px]"
|
||||
style={{
|
||||
minWidth: isStaticOption ? '48px' : '56px',
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
trigger="hover"
|
||||
content={I18n.t(
|
||||
'workflow_question_dynamic',
|
||||
{},
|
||||
'dynamicOption',
|
||||
)}
|
||||
>
|
||||
<span>{I18n.t('workflow_question_az', {}, 'A~Z')}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-full items-center">
|
||||
<ValueExpressionInputField
|
||||
name={'questionParams.dynamic_option'}
|
||||
availableFileTypes={[ViewVariableType.ArrayString]}
|
||||
disabledTypes={ViewVariableType.getComplement([
|
||||
ViewVariableType.ArrayString,
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SortableList
|
||||
onSortEnd={({ from, to }) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
updateSortedPortLines(from, to);
|
||||
move(from, to);
|
||||
setMovePostion({
|
||||
start: from,
|
||||
end: to,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex flex-col gap-[8px] text-xs leading-[24px]">
|
||||
{value?.map((_item, index) => (
|
||||
<AnswerItemField
|
||||
name={`${name}.${index}.name`}
|
||||
answerOptions={value}
|
||||
optionPlaceholder={optionPlaceholder}
|
||||
optionIndex={index}
|
||||
movePostion={movePostion}
|
||||
showOptionName={showOptionName}
|
||||
hasFeedback={false}
|
||||
onItemDelete={onItemDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableList>
|
||||
<AddOptionButton
|
||||
className="w-full mt-[8px]"
|
||||
dataTestId={getNodeSetterId('answer-option-add-btn')}
|
||||
readonly={readonly}
|
||||
onClick={() => {
|
||||
append({
|
||||
name: '',
|
||||
});
|
||||
}}
|
||||
children={
|
||||
<span className="text-[12px] font-medium">
|
||||
{I18n.t('workflow_question_add_option', {}, '新增选项')}
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center w-full text-xs mt-2">
|
||||
<div
|
||||
className="break-keep mr-[4px]"
|
||||
style={{
|
||||
minWidth: isStaticOption ? '48px' : '56px',
|
||||
marginLeft: isStaticOption ? '16px' : '0px',
|
||||
}}
|
||||
>
|
||||
{I18n.t('workflow_ques_ans_type_option_other', {}, 'other')}
|
||||
</div>
|
||||
<div className="space-x-1 w-full leading-[16px]">
|
||||
{defaultOptionText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnswerOptionField = ({
|
||||
name,
|
||||
optionPlaceholder,
|
||||
defaultOptionText,
|
||||
}) => (
|
||||
<FieldArray name={name} defaultValue={[{ name: '' }, { name: '' }]}>
|
||||
<AnswerOption
|
||||
optionPlaceholder={optionPlaceholder}
|
||||
defaultOptionText={defaultOptionText}
|
||||
/>
|
||||
</FieldArray>
|
||||
);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
const ASCII_TO_A_INDEX = 65; // 字母A对应的ASCII序号
|
||||
|
||||
export function convertNumberToLetters(n) {
|
||||
let result = '';
|
||||
while (n >= 0) {
|
||||
result = String.fromCharCode((n % 26) + ASCII_TO_A_INDEX) + result;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const generatePortId = (index: number) => `branch_${index}`;
|
||||
@@ -0,0 +1,48 @@
|
||||
.limit-wrapper {
|
||||
background: var(--Bg-COZ-bg-plus, #fcfcff);
|
||||
border: 0.67px solid
|
||||
var(--Stroke-COZ-stroke-primary, rgba(82, 100, 154, 13%));
|
||||
border-radius: 12px;
|
||||
|
||||
:global {
|
||||
.semi-slider {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-slider-handle {
|
||||
transform: translateX(-50%) translateY(100%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sys-popover-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
max-height: 800px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(--Fg-COZ-fg-plus, rgba(8, 13, 30, 90%));
|
||||
}
|
||||
|
||||
.slider-marks {
|
||||
top: 20px;
|
||||
|
||||
div {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 14px; /* 140% */
|
||||
color: var(--Fg-COZ-fg-primary, rgba(15, 21, 41, 82%));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useNodeTestId } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozSetting, IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Popover,
|
||||
Divider,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
|
||||
import { ExpressionEditorField } from '@/node-registries/common/fields';
|
||||
import { useWatch } from '@/form';
|
||||
import { CopyButton } from '@/components/copy-button';
|
||||
|
||||
import { SliderWithInputField } from './slider-with-input-field';
|
||||
|
||||
import styles from './question-limit.module.less';
|
||||
|
||||
const MAX_ROUND = 5;
|
||||
const MIN_ROUND = 1;
|
||||
|
||||
export const QuestionLimit = () => {
|
||||
const { getNodeSetterId } = useNodeTestId();
|
||||
const readonly = useReadonly();
|
||||
const systemPrompt = useWatch<string>({ name: 'llmParam.systemPrompt' });
|
||||
|
||||
const label = I18n.t(
|
||||
'workflow_ques_ans_type_direct_exrtact_title',
|
||||
{},
|
||||
"Extract variables from user's response.",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full mb-2">
|
||||
<Divider margin="12px" />
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<Typography.Text
|
||||
className="mr-[6px] text-xs"
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: label,
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
maxWidth: 'calc(100% - 24px)',
|
||||
color: '#1C1F23',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
<Popover
|
||||
autoAdjustOverflow={false}
|
||||
className={styles['limit-wrapper']}
|
||||
trigger="click"
|
||||
position="topRight"
|
||||
content={
|
||||
<div className={styles['sys-popover-content']}>
|
||||
<div className="flex items-center mb-[4px]">
|
||||
<span className={styles['content-title']}>
|
||||
{I18n.t(
|
||||
'workflow_ques_ans_type_direct_exrtact_context_setting',
|
||||
{},
|
||||
'Maximum dialogue rounds',
|
||||
)}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={I18n.t(
|
||||
'workflow_ques_ans_type_direct_context_setting_tooltips',
|
||||
{},
|
||||
'允许用户回答该问题的最多次数,当从用户的多次回答中获取不到必填的关键字段时,该工作流将会终止运行。',
|
||||
)}
|
||||
>
|
||||
<IconCozInfoCircle className="text-[#A7A9B0] text-[16px] ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-full relative mb-[16px]">
|
||||
<SliderWithInputField
|
||||
name="questionOutputs.limit"
|
||||
defaultValue={3}
|
||||
max={MAX_ROUND}
|
||||
min={MIN_ROUND}
|
||||
sliderStyle={{
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`w-full flex justify-between absolute ${styles['slider-marks']}`}
|
||||
>
|
||||
<div>{MIN_ROUND}</div>
|
||||
<div>{MAX_ROUND}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-[4px]">
|
||||
<div className="flex items-center">
|
||||
<span className={styles['content-title']}>
|
||||
{I18n.t('workflow_question_sp', {}, '系统提示词')}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={I18n.t(
|
||||
'workflow_question_sp_setting',
|
||||
{},
|
||||
'系统提示词设置',
|
||||
)}
|
||||
>
|
||||
<IconCozInfoCircle className="text-[#A7A9B0] text-[16px] ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{readonly ? <CopyButton value={systemPrompt ?? ''} /> : null}
|
||||
</div>
|
||||
<ExpressionEditorField
|
||||
name="llmParam.systemPrompt"
|
||||
defaultValue=""
|
||||
placeholder={I18n.t(
|
||||
'workflow_question_sp_placeholder',
|
||||
{},
|
||||
'支持额外的系统提示词,如设置人设和回复逻辑,使其追问语气更加自然',
|
||||
)}
|
||||
className="!p-[4px]"
|
||||
containerClassName="!bg-transparent"
|
||||
shouldUseContainerRef
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
size="default"
|
||||
color="secondary"
|
||||
data-testid={getNodeSetterId('question-limit-setting')}
|
||||
wrapperClass="flex justify-end"
|
||||
className="!p-[4px] !max-w-[24px] !min-w-[24px] !h-[24px]"
|
||||
icon={<IconCozSetting />}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { FILE_TYPES, ViewVariableType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { Section, useWatch } from '@/form';
|
||||
|
||||
import {
|
||||
DEFAULT_EXTRACT_OUTPUT,
|
||||
DEFAULT_ANSWER_OPTION_OUTPUT,
|
||||
} from '../constants';
|
||||
import {
|
||||
CheckboxWithTipsField,
|
||||
OutputsDisplayField,
|
||||
OutputsField,
|
||||
} from '../../common/fields';
|
||||
import { QuestionLimit } from './question-limit';
|
||||
|
||||
export const QuestionOutputs = () => {
|
||||
const answerType = useWatch({ name: 'questionParams.answer_type' });
|
||||
const extraOutput = useWatch<Boolean>({
|
||||
name: 'questionOutputs.extra_output',
|
||||
});
|
||||
const isOptionAnswer = answerType === 'option';
|
||||
|
||||
return (
|
||||
<Section
|
||||
title={I18n.t('workflow_detail_node_output')}
|
||||
tooltip={I18n.t('workflow_ques_output_tooltips')}
|
||||
actions={
|
||||
isOptionAnswer
|
||||
? []
|
||||
: [
|
||||
<CheckboxWithTipsField
|
||||
name="questionOutputs.extra_output"
|
||||
defaultValue={false}
|
||||
text={I18n.t(
|
||||
'workflow_ques_ans_type_direct_checkbox',
|
||||
{},
|
||||
'从回复中提取字段',
|
||||
)}
|
||||
itemTooltip={I18n.t(
|
||||
'workflow_ques_ans_type_direct_checkbox_tooltips',
|
||||
{},
|
||||
'开启后将从用户输入中提取信息',
|
||||
)}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
{isOptionAnswer ? (
|
||||
<OutputsDisplayField
|
||||
id={'question-node-option-output'}
|
||||
name={'questionOutputs.optionOutput'}
|
||||
defaultValue={DEFAULT_ANSWER_OPTION_OUTPUT}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<OutputsField
|
||||
title=""
|
||||
id={'question-node-user-output'}
|
||||
name={'questionOutputs.userOutput'}
|
||||
noCard
|
||||
jsonImport={false}
|
||||
disabled={true}
|
||||
allowAppendRootData={false}
|
||||
topLevelReadonly
|
||||
withRequired
|
||||
hasFeedback={false}
|
||||
/>
|
||||
{extraOutput ? (
|
||||
<>
|
||||
<QuestionLimit />
|
||||
<OutputsField
|
||||
title=""
|
||||
id={'question-node-extract-output'}
|
||||
name={'questionOutputs.extractOutput'}
|
||||
defaultValue={DEFAULT_EXTRACT_OUTPUT}
|
||||
hiddenTypes={[
|
||||
...FILE_TYPES,
|
||||
ViewVariableType.ArrayBoolean,
|
||||
ViewVariableType.ArrayInteger,
|
||||
ViewVariableType.ArrayNumber,
|
||||
ViewVariableType.ArrayObject,
|
||||
ViewVariableType.ArrayString,
|
||||
ViewVariableType.Object,
|
||||
]}
|
||||
noCard
|
||||
withRequired
|
||||
jsonImport={false}
|
||||
hasFeedback={false}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { SliderWithInput as SliderWithInputLegacy } from '@/form-extensions/components/slider-with-input';
|
||||
import { useField, withField } from '@/form';
|
||||
|
||||
const SliderWithInput = props => {
|
||||
const { value, onChange, readonly } = useField<string | number>();
|
||||
|
||||
return (
|
||||
<SliderWithInputLegacy
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readonly={!!readonly}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SliderWithInputField = withField(SliderWithInput);
|
||||
@@ -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 { nanoid } from '@flowgram-adapter/free-layout-editor';
|
||||
import { ViewVariableType } from '@coze-workflow/nodes';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const DEFAULT_USER_RESPONSE_PARAM_NAME = 'USER_RESPONSE';
|
||||
export const DEFAULT_OPTION_ID_NAME = 'optionId';
|
||||
export const DEFAULT_OPTION_CONTENT_NAME = 'optionContent';
|
||||
|
||||
export const DEFAULT_OUTPUT_NAMES = [
|
||||
DEFAULT_USER_RESPONSE_PARAM_NAME,
|
||||
DEFAULT_OPTION_ID_NAME,
|
||||
DEFAULT_OPTION_CONTENT_NAME,
|
||||
];
|
||||
|
||||
export const DEFAULT_USE_RESPONSE = [
|
||||
{
|
||||
key: nanoid(),
|
||||
name: DEFAULT_USER_RESPONSE_PARAM_NAME,
|
||||
type: ViewVariableType.String,
|
||||
required: true,
|
||||
description: I18n.t(
|
||||
'workflow_ques_ans_type_direct_key_decr',
|
||||
{},
|
||||
'用户本轮对话输入内容',
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_EXTRACT_OUTPUT = [
|
||||
{
|
||||
key: nanoid(),
|
||||
name: 'output',
|
||||
type: ViewVariableType.String,
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ANSWER_OPTION_OUTPUT = [
|
||||
{
|
||||
key: nanoid(),
|
||||
name: DEFAULT_OPTION_ID_NAME,
|
||||
type: ViewVariableType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: nanoid(),
|
||||
name: DEFAULT_OPTION_CONTENT_NAME,
|
||||
type: ViewVariableType.String,
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -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 { get } from 'lodash-es';
|
||||
import { variableUtils } from '@coze-workflow/variable';
|
||||
import { getDefaultLLMParams, formatModelData } from '@coze-workflow/nodes';
|
||||
|
||||
import { OptionType } from '@/constants/question-settings';
|
||||
|
||||
import {
|
||||
DEFAULT_USE_RESPONSE,
|
||||
DEFAULT_USER_RESPONSE_PARAM_NAME,
|
||||
DEFAULT_EXTRACT_OUTPUT,
|
||||
DEFAULT_ANSWER_OPTION_OUTPUT,
|
||||
DEFAULT_OUTPUT_NAMES,
|
||||
} from './constants';
|
||||
|
||||
export function transformOnInit(value, context) {
|
||||
const { playgroundContext } = context;
|
||||
const { variableService } = playgroundContext;
|
||||
const { models } = playgroundContext;
|
||||
const { inputs = {}, outputs = DEFAULT_USE_RESPONSE, nodeMeta } = value || {};
|
||||
|
||||
const {
|
||||
inputParameters = [],
|
||||
answer_type = 'text',
|
||||
dynamic_option,
|
||||
option_type = OptionType.Static,
|
||||
extra_output,
|
||||
question,
|
||||
options,
|
||||
limit = 3,
|
||||
} = inputs;
|
||||
|
||||
const isAnswerTypeOption = answer_type === 'option';
|
||||
|
||||
const userOutput = (outputs || []).filter(
|
||||
item => item.name === DEFAULT_USER_RESPONSE_PARAM_NAME,
|
||||
);
|
||||
|
||||
const extractOutput = (outputs || []).filter(item =>
|
||||
isAnswerTypeOption
|
||||
? !DEFAULT_OUTPUT_NAMES.includes(item.name)
|
||||
: item.name !== DEFAULT_USER_RESPONSE_PARAM_NAME,
|
||||
);
|
||||
|
||||
let llmParam = get(value, 'inputs.llmParam');
|
||||
// 初次拖入画布时:从后端返回值里,解析出来默认值。
|
||||
if (!llmParam) {
|
||||
llmParam = getDefaultLLMParams(models);
|
||||
}
|
||||
|
||||
return {
|
||||
llmParam,
|
||||
nodeMeta,
|
||||
questionOutputs: {
|
||||
limit,
|
||||
extra_output: isAnswerTypeOption ? false : extra_output,
|
||||
userOutput: userOutput.length > 0 ? userOutput : DEFAULT_USE_RESPONSE,
|
||||
extractOutput:
|
||||
extractOutput.length > 0 ? extractOutput : DEFAULT_EXTRACT_OUTPUT,
|
||||
optionOutput: DEFAULT_ANSWER_OPTION_OUTPUT,
|
||||
},
|
||||
outputs,
|
||||
inputParameters: inputParameters ?? [],
|
||||
questionParams: {
|
||||
answer_type,
|
||||
question,
|
||||
options,
|
||||
option_type,
|
||||
dynamic_option: variableUtils.valueExpressionToVO(
|
||||
dynamic_option,
|
||||
variableService,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transformOnSubmit(value, context) {
|
||||
const { playgroundContext, node } = context;
|
||||
const { variableService } = playgroundContext;
|
||||
|
||||
const { models } = playgroundContext;
|
||||
|
||||
const {
|
||||
llmParam,
|
||||
nodeMeta,
|
||||
inputParameters,
|
||||
questionOutputs,
|
||||
outputs,
|
||||
questionParams,
|
||||
} = value;
|
||||
|
||||
const { limit, extra_output } = questionOutputs;
|
||||
|
||||
const { question, answer_type, options, dynamic_option, option_type } =
|
||||
questionParams;
|
||||
|
||||
const modelMeta = models.find(m => m.model_type === llmParam?.modelType);
|
||||
|
||||
return {
|
||||
inputs: {
|
||||
llmParam: {
|
||||
...formatModelData(llmParam, modelMeta),
|
||||
systemPrompt: llmParam?.systemPrompt ?? '',
|
||||
},
|
||||
inputParameters: inputParameters ?? [],
|
||||
extra_output,
|
||||
answer_type,
|
||||
option_type,
|
||||
dynamic_option: !dynamic_option
|
||||
? null
|
||||
: variableUtils.valueExpressionToDTO(dynamic_option, variableService, {
|
||||
node,
|
||||
}),
|
||||
question,
|
||||
options,
|
||||
limit,
|
||||
},
|
||||
nodeMeta,
|
||||
outputs,
|
||||
};
|
||||
}
|
||||
@@ -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 { syncQuestionOutputsEffect } from './sync-question-outputs';
|
||||
export { syncQuestionAnswerTypeEffect } from './sync-question-answer-type';
|
||||
export { syncQuestionOptionTypeEffect } from './sync-question-option-type';
|
||||
@@ -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 { isEqual, get } from 'lodash-es';
|
||||
import {
|
||||
type Effect,
|
||||
FlowNodeFormData,
|
||||
type FormModelV2,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowNodePortsData } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { formatOutput } from '../utils';
|
||||
|
||||
export const syncQuestionAnswerTypeEffect: Effect = props => {
|
||||
const { value, formValues, context } = props;
|
||||
const { node } = context;
|
||||
|
||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||
const portsData = node.getData<WorkflowNodePortsData>(WorkflowNodePortsData);
|
||||
const outputs = get(formValues, 'outputs');
|
||||
|
||||
if (value === 'text') {
|
||||
portsData.updateStaticPorts([
|
||||
{
|
||||
type: 'input',
|
||||
},
|
||||
{
|
||||
type: 'output',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
portsData.updateStaticPorts([
|
||||
{
|
||||
type: 'input',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// 表单初始化时获取不到值,需要延时一会
|
||||
setTimeout(() => {
|
||||
let syncOutputValue: unknown = [];
|
||||
if (value === 'text') {
|
||||
if (outputs) {
|
||||
const questionOutputs = get(formValues, 'questionOutputs');
|
||||
syncOutputValue = formatOutput(questionOutputs);
|
||||
}
|
||||
} else {
|
||||
const optionOutput = get(formValues, 'questionOutputs.optionOutput');
|
||||
syncOutputValue = optionOutput;
|
||||
}
|
||||
|
||||
// 将 questionOutput 的值同步到 output 上
|
||||
if (outputs && !isEqual(outputs, syncOutputValue)) {
|
||||
formModel.setValueIn('outputs', syncOutputValue);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Effect,
|
||||
FlowNodeFormData,
|
||||
type FormModelV2,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { OptionType } from '@/constants/question-settings';
|
||||
|
||||
export const syncQuestionOptionTypeEffect: Effect = props => {
|
||||
const { value, context } = props;
|
||||
const { node } = context;
|
||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||
if (value === OptionType.Dynamic) {
|
||||
return;
|
||||
}
|
||||
formModel.setValueIn('questionParams.dynamic_option', null);
|
||||
};
|
||||
@@ -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 { get } from 'lodash-es';
|
||||
import {
|
||||
type Effect,
|
||||
FlowNodeFormData,
|
||||
type FormModelV2,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { formatOutput } from '../utils';
|
||||
|
||||
export const syncQuestionOutputsEffect: Effect = props => {
|
||||
const { value, formValues, context } = props;
|
||||
const { node } = context;
|
||||
|
||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||
const outputs = get(formValues, 'outputs');
|
||||
|
||||
// 将 questionOutputs 的值同步到outputs上
|
||||
if (outputs) {
|
||||
formModel.setValueIn('outputs', formatOutput(value));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
ValidateTrigger,
|
||||
type FormMetaV2,
|
||||
DataEvent,
|
||||
type EffectOptions,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { provideNodeOutputVariablesEffect } from '@/nodes-v2/materials/provide-node-output-variables';
|
||||
import { nodeMetaValidate } from '@/nodes-v2/materials/node-meta-validate';
|
||||
import { fireNodeTitleChange } from '@/nodes-v2/materials/fire-node-title-change';
|
||||
import { createValueExpressionInputValidate } from '@/nodes-v2/materials/create-value-expression-input-validate';
|
||||
import { createNodeInputNameValidate } from '@/nodes-v2/components/node-input-name/validate';
|
||||
import { valueExpressionValidator } from '@/form-extensions/validators';
|
||||
import { OptionType } from '@/constants/question-settings';
|
||||
|
||||
import { outputTreeMetaValidator } from '../common/fields/outputs';
|
||||
import FormRender from './form';
|
||||
import {
|
||||
syncQuestionAnswerTypeEffect,
|
||||
syncQuestionOptionTypeEffect,
|
||||
syncQuestionOutputsEffect,
|
||||
} from './effects';
|
||||
import { transformOnInit, transformOnSubmit } from './data-transformer';
|
||||
|
||||
const questionFieldName = 'questionParams.question';
|
||||
const questionOptionsFieldName = 'questionParams.options.*.name';
|
||||
const questionDynamicFieldName = 'questionParams.dynamic_option';
|
||||
|
||||
export const QUESTION_FORM_META: FormMetaV2<FormData> = {
|
||||
// 节点表单渲染
|
||||
render: () => <FormRender />,
|
||||
|
||||
// 验证触发时机
|
||||
validateTrigger: ValidateTrigger.onBlur,
|
||||
|
||||
// 验证规则
|
||||
validate: {
|
||||
nodeMeta: nodeMetaValidate,
|
||||
'inputParameters.*.name': createNodeInputNameValidate({
|
||||
getNames: ({ formValues }) =>
|
||||
(get(formValues, 'inputParameters') || []).map(item => item.name),
|
||||
}),
|
||||
'inputParameters.*.input': createValueExpressionInputValidate({
|
||||
required: true,
|
||||
}),
|
||||
[questionFieldName]: ({ value }) =>
|
||||
value
|
||||
? undefined
|
||||
: I18n.t('workflow_detail_node_error_empty', {}, '参数值不可为空'),
|
||||
[questionOptionsFieldName]: ({ value, formValues }) => {
|
||||
const anwserType = get(formValues, 'questionParams.answer_type');
|
||||
const optionType = get(formValues, 'questionParams.option_type');
|
||||
if (anwserType !== 'option' || optionType !== OptionType.Static) {
|
||||
return undefined;
|
||||
}
|
||||
if (!value) {
|
||||
return I18n.t('workflow_ques_option_notempty', {}, '选项内容不可为空');
|
||||
}
|
||||
const options = get(formValues, 'questionParams.options');
|
||||
|
||||
return options.filter(option => option?.name === value)?.length > 1
|
||||
? I18n.t('workflow_ques_ans_testrun_dulpicate', {}, '选项内容不可重复')
|
||||
: undefined;
|
||||
},
|
||||
[questionDynamicFieldName]: ({ value, formValues, context }) => {
|
||||
const { node, playgroundContext } = context;
|
||||
const anwserType = get(formValues, 'questionParams.answer_type');
|
||||
const optionType = get(formValues, 'questionParams.option_type');
|
||||
|
||||
if (anwserType !== 'option' || optionType !== OptionType.Dynamic) {
|
||||
return undefined;
|
||||
}
|
||||
return valueExpressionValidator({
|
||||
value,
|
||||
playgroundContext,
|
||||
node,
|
||||
required: true,
|
||||
});
|
||||
},
|
||||
'questionOutputs.extractOutput': outputTreeMetaValidator,
|
||||
},
|
||||
|
||||
// 副作用管理
|
||||
effect: {
|
||||
'questionParams.answer_type': [
|
||||
{
|
||||
effect: syncQuestionAnswerTypeEffect,
|
||||
event: DataEvent.onValueChange,
|
||||
},
|
||||
{
|
||||
effect: syncQuestionAnswerTypeEffect,
|
||||
event: DataEvent.onValueInit,
|
||||
},
|
||||
] as unknown as EffectOptions[],
|
||||
'questionParams.option_type': [
|
||||
{
|
||||
effect: syncQuestionOptionTypeEffect,
|
||||
event: DataEvent.onValueChange,
|
||||
},
|
||||
],
|
||||
questionOutputs: [
|
||||
{
|
||||
effect: syncQuestionOutputsEffect,
|
||||
event: DataEvent.onValueChange,
|
||||
},
|
||||
] as unknown as EffectOptions[],
|
||||
nodeMeta: fireNodeTitleChange,
|
||||
outputs: provideNodeOutputVariablesEffect,
|
||||
},
|
||||
|
||||
// 节点后端数据 -> 前端表单数据
|
||||
formatOnInit: transformOnInit,
|
||||
|
||||
// 前端表单数据 -> 节点后端数据
|
||||
formatOnSubmit: transformOnSubmit,
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { NodeConfigForm } from '@/node-registries/common/components';
|
||||
import { Section } from '@/form';
|
||||
import { AnswerType } from '@/constants/question-settings';
|
||||
|
||||
import {
|
||||
ModelSelectField,
|
||||
InputsParametersField,
|
||||
ExpressionEditorField,
|
||||
RadioSetterField,
|
||||
} from '../common/fields';
|
||||
import { QuestionOutputs } from './components/question-outputs';
|
||||
import { AnswerOptionField } from './components/answer-option-field';
|
||||
|
||||
const Render = () => (
|
||||
<NodeConfigForm>
|
||||
<ModelSelectField
|
||||
name="llmParam"
|
||||
title={I18n.t('workflow_detail_llm_model')}
|
||||
/>
|
||||
<InputsParametersField
|
||||
name="inputParameters"
|
||||
tooltip={I18n.t(
|
||||
'workflow_ques_input_tooltips',
|
||||
{},
|
||||
'输入需要添加到问题的参数,这些参数可以被下方的问题引用',
|
||||
)}
|
||||
/>
|
||||
<Section
|
||||
title={I18n.t('workflow_ques_content', {}, '提问内容')}
|
||||
tooltip={I18n.t(
|
||||
'workflow_ques_content_tooltips',
|
||||
{},
|
||||
'用于对用户发出提问的具体内容描述',
|
||||
)}
|
||||
>
|
||||
<div className="w-full mb-[12px]">
|
||||
<ExpressionEditorField
|
||||
name="questionParams.question"
|
||||
dataTestName="/questionParams/question"
|
||||
placeholder={I18n.t('workflow_ques_content_placeholder')}
|
||||
className="!p-[4px]"
|
||||
containerClassName="!bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<Section
|
||||
headerClassName="!mb-0"
|
||||
title={
|
||||
<div className="text-xs font-normal">
|
||||
{I18n.t('workflow_ques_ans_type', {}, '请选择回答类型')}
|
||||
</div>
|
||||
}
|
||||
noPadding
|
||||
collapsible={false}
|
||||
>
|
||||
<RadioSetterField
|
||||
name="questionParams.answer_type"
|
||||
defaultValue={AnswerType.Text}
|
||||
options={{
|
||||
key: 'questionParams.answer_type',
|
||||
mode: 'card',
|
||||
direction: 'vertical',
|
||||
customClassName: 'pt-[4px] gap-y-[4px]',
|
||||
options: [
|
||||
{
|
||||
value: AnswerType.Text,
|
||||
label: I18n.t('workflow_ques_ans_type_direct', {}, '直接回答'),
|
||||
},
|
||||
{
|
||||
value: AnswerType.Option,
|
||||
label: I18n.t('workflow_ques_ans_type_option', {}, '选项回答'),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
<AnswerOptionField
|
||||
name="questionParams.options"
|
||||
optionPlaceholder={I18n.t(
|
||||
'workflow_ans_content_placeholder',
|
||||
{},
|
||||
'可以使用{{变量名}}引入输入参数中的变量',
|
||||
)}
|
||||
defaultOptionText={I18n.t(
|
||||
'workflow_ques_ans_type_option_other_placeholder',
|
||||
{},
|
||||
'此选项对用户不可见,当用户回复无关内容时,走此分支',
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
<QuestionOutputs />
|
||||
</NodeConfigForm>
|
||||
);
|
||||
|
||||
export default Render;
|
||||
@@ -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 { QUESTION_NODE_REGISTRY } from './node-registry';
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
DEFAULT_NODE_META_PATH,
|
||||
DEFAULT_NODE_SIZE,
|
||||
DEFAULT_OUTPUTS_PATH,
|
||||
type WorkflowNodeRegistry,
|
||||
} from '@coze-workflow/nodes';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import { type NodeTestMeta } from '@/test-run-kit';
|
||||
|
||||
import { test } from './node-test';
|
||||
import { QUESTION_FORM_META } from './form-meta';
|
||||
|
||||
export const QUESTION_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> = {
|
||||
type: StandardNodeType.Question,
|
||||
meta: {
|
||||
nodeDTOType: StandardNodeType.Question,
|
||||
size: { width: DEFAULT_NODE_SIZE.width, height: 156.7 },
|
||||
nodeMetaPath: DEFAULT_NODE_META_PATH,
|
||||
outputsPath: DEFAULT_OUTPUTS_PATH,
|
||||
useDynamicPort: true,
|
||||
inputParametersPath: '/inputParameters',
|
||||
getLLMModelIdsByNodeJSON: nodeJSON =>
|
||||
nodeJSON?.data?.inputs?.llmParam?.modelType,
|
||||
test,
|
||||
helpLink: '/open/docs/guides/question_node',
|
||||
},
|
||||
formMeta: QUESTION_FORM_META,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type IFormSchema } from '@coze-workflow/test-run-next';
|
||||
|
||||
import { generateParametersToProperties } from '@/test-run-kit';
|
||||
import { type NodeTestMeta } from '@/test-run-kit';
|
||||
import { AnswerType, OptionType } from '@/constants/question-settings';
|
||||
|
||||
export const test: NodeTestMeta = {
|
||||
generateFormInputProperties(node) {
|
||||
const formData = node
|
||||
.getData(FlowNodeFormData)
|
||||
.formModel.getFormItemValueByPath('/');
|
||||
const inputParameters = formData?.inputParameters;
|
||||
|
||||
const inputProperties = generateParametersToProperties(inputParameters, {
|
||||
node,
|
||||
});
|
||||
const answerType = formData?.questionParams?.answer_type;
|
||||
const optionType = formData?.questionParams?.option_type;
|
||||
let dynamicProperties: IFormSchema['properties'] = {};
|
||||
if (answerType === AnswerType.Option && optionType === OptionType.Dynamic) {
|
||||
const dynamicOption = formData?.questionParams?.dynamic_option;
|
||||
dynamicProperties = generateParametersToProperties(
|
||||
[
|
||||
{
|
||||
name: 'dynamic_option',
|
||||
input: dynamicOption,
|
||||
},
|
||||
],
|
||||
{ node },
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...inputProperties,
|
||||
...dynamicProperties,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export interface QuestionOutputsValue {
|
||||
userOutput: Array<any>;
|
||||
extractOutput: Array<any>;
|
||||
extra_output: boolean;
|
||||
}
|
||||
|
||||
export const formatOutput = (value: QuestionOutputsValue) => {
|
||||
const { userOutput, extractOutput, extra_output } = value;
|
||||
|
||||
if (extra_output) {
|
||||
return [...userOutput, ...extractOutput];
|
||||
}
|
||||
return userOutput;
|
||||
};
|
||||
Reference in New Issue
Block a user