Feat: Add FormDrawer to agent page. #3221 (#5323)

### What problem does this PR solve?

Feat: Add FormDrawer to agent page. #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-02-25 11:32:01 +08:00
committed by GitHub
parent b3d579e2c1
commit 9c9f2dbe3f
63 changed files with 4005 additions and 70 deletions

View File

@ -0,0 +1,225 @@
import { useTranslate } from '@/hooks/common-hooks';
import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
import { useUpdateNodeInternals } from '@xyflow/react';
import {
Button,
Collapse,
Flex,
Form,
FormListFieldData,
Input,
Select,
} from 'antd';
import { FormInstance } from 'antd/lib';
import { humanId } from 'human-id';
import trim from 'lodash/trim';
import {
ChangeEventHandler,
FocusEventHandler,
useCallback,
useEffect,
useState,
} from 'react';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import styles from './index.less';
interface IProps {
nodeId?: string;
}
interface INameInputProps {
value?: string;
onChange?: (value: string) => void;
otherNames?: string[];
validate(errors: string[]): void;
}
const getOtherFieldValues = (
form: FormInstance,
formListName: string = 'items',
field: FormListFieldData,
latestField: string,
) =>
(form.getFieldValue([formListName]) ?? [])
.map((x: any) => x[latestField])
.filter(
(x: string) =>
x !== form.getFieldValue([formListName, field.name, latestField]),
);
const NameInput = ({
value,
onChange,
otherNames,
validate,
}: INameInputProps) => {
const [name, setName] = useState<string | undefined>();
const { t } = useTranslate('flow');
const handleNameChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
const val = e.target.value;
// trigger validation
if (otherNames?.some((x) => x === val)) {
validate([t('nameRepeatedMsg')]);
} else if (trim(val) === '') {
validate([t('nameRequiredMsg')]);
} else {
validate([]);
}
setName(val);
},
[otherNames, validate, t],
);
const handleNameBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
const val = e.target.value;
if (otherNames?.every((x) => x !== val) && trim(val) !== '') {
onChange?.(val);
}
},
[onChange, otherNames],
);
useEffect(() => {
setName(value);
}, [value]);
return (
<Input
value={name}
onChange={handleNameChange}
onBlur={handleNameBlur}
></Input>
);
};
const FormSet = ({ nodeId, field }: IProps & { field: FormListFieldData }) => {
const form = Form.useFormInstance();
const { t } = useTranslate('flow');
const buildCategorizeToOptions = useBuildFormSelectOptions(
Operator.Categorize,
nodeId,
);
return (
<section>
<Form.Item
label={t('categoryName')}
name={[field.name, 'name']}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: t('nameMessage'),
},
]}
>
<NameInput
otherNames={getOtherFieldValues(form, 'items', field, 'name')}
validate={(errors: string[]) =>
form.setFields([
{
name: ['items', field.name, 'name'],
errors,
},
])
}
></NameInput>
</Form.Item>
<Form.Item label={t('description')} name={[field.name, 'description']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label={t('examples')} name={[field.name, 'examples']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label={t('nextStep')} name={[field.name, 'to']}>
<Select
allowClear
options={buildCategorizeToOptions(
getOtherFieldValues(form, 'items', field, 'to'),
)}
/>
</Form.Item>
<Form.Item hidden name={[field.name, 'index']}>
<Input />
</Form.Item>
</section>
);
};
const DynamicCategorize = ({ nodeId }: IProps) => {
const updateNodeInternals = useUpdateNodeInternals();
const form = Form.useFormInstance();
const { t } = useTranslate('flow');
return (
<>
<Form.List name="items">
{(fields, { add, remove }) => {
const handleAdd = () => {
const idx = form.getFieldValue([
'items',
fields.at(-1)?.name,
'index',
]);
add({
name: humanId(),
index: fields.length === 0 ? 0 : idx + 1,
});
if (nodeId) updateNodeInternals(nodeId);
};
return (
<Flex gap={18} vertical>
{fields.map((field) => (
<Collapse
size="small"
key={field.key}
className={styles.caseCard}
items={[
{
key: field.key,
label: (
<div className="flex justify-between">
<span>
{form.getFieldValue(['items', field.name, 'name'])}
</span>
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
</div>
),
children: (
<FormSet nodeId={nodeId} field={field}></FormSet>
),
},
]}
></Collapse>
))}
<Button
type="dashed"
onClick={handleAdd}
block
className={styles.addButton}
icon={<PlusOutlined />}
>
{t('addCategory')}
</Button>
</Flex>
);
}}
</Form.List>
</>
);
};
export default DynamicCategorize;

View File

@ -0,0 +1,45 @@
import {
ICategorizeItem,
ICategorizeItemResult,
} from '@/interfaces/database/flow';
import omit from 'lodash/omit';
import { useCallback } from 'react';
import { IOperatorForm } from '../../interface';
/**
* Convert the list in the following form into an object
* {
"items": [
{
"name": "Categorize 1",
"description": "111",
"examples": "ddd",
"to": "Retrieval:LazyEelsStick"
}
]
}
*/
const buildCategorizeObjectFromList = (list: Array<ICategorizeItem>) => {
return list.reduce<ICategorizeItemResult>((pre, cur) => {
if (cur?.name) {
pre[cur.name] = omit(cur, 'name');
}
return pre;
}, {});
};
export const useHandleFormValuesChange = ({
onValuesChange,
}: IOperatorForm) => {
const handleValuesChange = useCallback(
(changedValues: any, values: any) => {
onValuesChange?.(changedValues, {
...omit(values, 'items'),
category_description: buildCategorizeObjectFromList(values.items),
});
},
[onValuesChange],
);
return { handleValuesChange };
};

View File

@ -0,0 +1,13 @@
@lightBackgroundColor: rgba(150, 150, 150, 0.07);
@darkBackgroundColor: rgba(150, 150, 150, 0.12);
.caseCard {
:global(.ant-collapse-content) {
background-color: @darkBackgroundColor;
}
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

View File

@ -0,0 +1,43 @@
import LLMSelect from '@/components/llm-select';
import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
import DynamicCategorize from './dynamic-categorize';
import { useHandleFormValuesChange } from './hooks';
const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const { handleValuesChange } = useHandleFormValuesChange({
form,
nodeId: node?.id,
onValuesChange,
});
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={handleValuesChange}
initialValues={{ items: [{}] }}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<MessageHistoryWindowSizeItem
initialValue={1}
></MessageHistoryWindowSizeItem>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
</Form>
);
};
export default CategorizeForm;