Feat: Construct a dynamic variable assignment form
This commit is contained in:
parent
82aaac71b5
commit
535cb7b009
7 changed files with 253 additions and 73 deletions
|
|
@ -13,7 +13,7 @@ export const LogicalOperatorIcon = function OperatorIcon({
|
|||
<IconFont
|
||||
name={icon}
|
||||
className={cn('size-4', {
|
||||
'rotate-180': value === '>',
|
||||
'rotate-180': value === ComparisonOperator.GreatThan,
|
||||
})}
|
||||
></IconFont>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function AccordionOperators({
|
|||
Operator.Code,
|
||||
Operator.StringTransform,
|
||||
Operator.DataOperations,
|
||||
// Operator.VariableAssigner,
|
||||
Operator.VariableAssigner,
|
||||
Operator.VariableAggregator,
|
||||
]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
|
|
|
|||
|
|
@ -814,3 +814,29 @@ export enum JsonSchemaDataType {
|
|||
Array = 'array',
|
||||
Object = 'object',
|
||||
}
|
||||
|
||||
export enum VariableAssignerLogicalOperator {
|
||||
Overwrite = 'overwrite',
|
||||
Clear = 'clear',
|
||||
Set = 'set',
|
||||
}
|
||||
|
||||
export enum VariableAssignerLogicalNumberOperator {
|
||||
Overwrite = VariableAssignerLogicalOperator.Overwrite,
|
||||
Clear = VariableAssignerLogicalOperator.Clear,
|
||||
Set = VariableAssignerLogicalOperator.Set,
|
||||
Add = '+=',
|
||||
Subtract = '-=',
|
||||
Multiply = '*=',
|
||||
Divide = '/=',
|
||||
}
|
||||
|
||||
export enum VariableAssignerLogicalArrayOperator {
|
||||
Overwrite = VariableAssignerLogicalOperator.Overwrite,
|
||||
Clear = VariableAssignerLogicalOperator.Clear,
|
||||
Set = VariableAssignerLogicalOperator.Set,
|
||||
Append = 'append',
|
||||
Extend = 'extend',
|
||||
RemoveFirst = 'remove_first',
|
||||
RemoveLast = 'remove_last',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ import { PropsWithChildren, ReactNode, useCallback } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { JsonSchemaDataType } from '../../constant';
|
||||
import { useGetStructuredOutputByValue } from '../../hooks/use-build-structured-output';
|
||||
import {
|
||||
hasJsonSchemaChild,
|
||||
hasSpecificTypeChild,
|
||||
} from '../../utils/filter-agent-structured-output';
|
||||
import { hasSpecificTypeChild } from '../../utils/filter-agent-structured-output';
|
||||
|
||||
type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode };
|
||||
|
||||
|
|
@ -101,8 +98,9 @@ export function StructuredOutputSecondaryMenu({
|
|||
);
|
||||
|
||||
if (
|
||||
!hasJsonSchemaChild(structuredOutput) ||
|
||||
(!isEmpty(types) && !hasSpecificTypeChild(structuredOutput, types))
|
||||
!isEmpty(types) &&
|
||||
!hasSpecificTypeChild(structuredOutput, types) &&
|
||||
!types.some((x) => x === JsonSchemaDataType.Object)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
import NumberInput from '@/components/originui/number-input';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
JsonSchemaDataType,
|
||||
VariableAssignerLogicalNumberOperator,
|
||||
VariableAssignerLogicalOperator,
|
||||
} from '../../constant';
|
||||
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
|
||||
import { DynamicFormHeader } from '../components/dynamic-fom-header';
|
||||
import { PromptEditor } from '../components/prompt-editor';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
import { useBuildLogicalOptions } from './use-build-logical-options';
|
||||
|
||||
type SelectKeysProps = {
|
||||
name: string;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
keyField?: string;
|
||||
valueField?: string;
|
||||
operatorField?: string;
|
||||
};
|
||||
|
||||
type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;
|
||||
|
||||
type RadioButtonProps = Partial<
|
||||
Omit<RadioGroupProps, 'onValueChange'> & {
|
||||
onChange: RadioGroupProps['onValueChange'];
|
||||
}
|
||||
>;
|
||||
|
||||
function RadioButton({ value, onChange }: RadioButtonProps) {
|
||||
return (
|
||||
<RadioGroup
|
||||
defaultValue="yes"
|
||||
className="flex"
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="yes" id="r1" />
|
||||
<Label htmlFor="r1">Yes</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="no" id="r2" />
|
||||
<Label htmlFor="r2">No</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export function DynamicVariables({
|
||||
name,
|
||||
label,
|
||||
tooltip,
|
||||
keyField = 'variable',
|
||||
valueField = 'parameter',
|
||||
operatorField = 'operator',
|
||||
}: SelectKeysProps) {
|
||||
const form = useFormContext();
|
||||
const { getType } = useGetVariableLabelOrTypeByValue();
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const { buildLogicalOptions } = useBuildLogicalOptions();
|
||||
|
||||
const getVariableType = useCallback(
|
||||
(keyFieldName: string) => {
|
||||
const key = form.getValues(keyFieldName);
|
||||
return getType(key);
|
||||
},
|
||||
[form, getType],
|
||||
);
|
||||
|
||||
const renderParameter = useCallback(
|
||||
(keyFieldName: string, operatorFieldName: string) => {
|
||||
const logicalOperator = form.getValues(operatorFieldName);
|
||||
|
||||
if (logicalOperator === VariableAssignerLogicalOperator.Clear) {
|
||||
return null;
|
||||
} else if (
|
||||
logicalOperator === VariableAssignerLogicalOperator.Overwrite
|
||||
) {
|
||||
return <PromptEditor showToolbar={false} multiLine={false} />;
|
||||
} else if (logicalOperator === VariableAssignerLogicalOperator.Set) {
|
||||
const type = getVariableType(keyFieldName);
|
||||
|
||||
if (type === JsonSchemaDataType.Boolean) {
|
||||
return <RadioButton></RadioButton>;
|
||||
}
|
||||
} else if (
|
||||
Object.values(VariableAssignerLogicalNumberOperator).some(
|
||||
(x) => logicalOperator === x,
|
||||
)
|
||||
) {
|
||||
return <NumberInput className="w-full"></NumberInput>;
|
||||
}
|
||||
},
|
||||
[form, getVariableType],
|
||||
);
|
||||
|
||||
const handleVariableChange = useCallback(
|
||||
(operatorFieldAlias: string, valueFieldAlias: string) => () => {
|
||||
form.setValue(
|
||||
operatorFieldAlias,
|
||||
VariableAssignerLogicalOperator.Overwrite,
|
||||
);
|
||||
form.setValue(valueFieldAlias, undefined);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<DynamicFormHeader
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
onClick={() => append({ [keyField]: '', [valueField]: '' })}
|
||||
></DynamicFormHeader>
|
||||
|
||||
<div className="space-y-5">
|
||||
{fields.map((field, index) => {
|
||||
const keyFieldAlias = `${name}.${index}.${keyField}`;
|
||||
const valueFieldAlias = `${name}.${index}.${valueField}`;
|
||||
const operatorFieldAlias = `${name}.${index}.${operatorField}`;
|
||||
|
||||
return (
|
||||
<section key={field.id} className="flex gap-2">
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
<div className="flex items-center">
|
||||
<QueryVariable
|
||||
name={keyFieldAlias}
|
||||
hideLabel
|
||||
className="flex-1 min-w-0"
|
||||
onChange={handleVariableChange(
|
||||
operatorFieldAlias,
|
||||
valueFieldAlias,
|
||||
)}
|
||||
></QueryVariable>
|
||||
|
||||
<Separator className="w-2" />
|
||||
|
||||
<RAGFlowFormItem name={operatorFieldAlias} className="w-1/3">
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
options={buildLogicalOptions(
|
||||
getVariableType(keyFieldAlias),
|
||||
)}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
|
||||
{renderParameter(keyFieldAlias, operatorFieldAlias)}
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X />
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,97 +1,48 @@
|
|||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { memo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
JsonSchemaDataType,
|
||||
Operations,
|
||||
initialDataOperationsValues,
|
||||
} from '../../constant';
|
||||
import { initialDataOperationsValues } from '../../constant';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { buildOutputList } from '../../utils/build-output-list';
|
||||
import { FormWrapper } from '../components/form-wrapper';
|
||||
import { Output, OutputSchema } from '../components/output';
|
||||
import { QueryVariableList } from '../components/query-variable-list';
|
||||
import { DynamicVariables } from './dynamic-variables';
|
||||
|
||||
export const RetrievalPartialSchema = {
|
||||
query: z.array(z.object({ input: z.string().optional() })),
|
||||
operations: z.string(),
|
||||
select_keys: z.array(z.object({ name: z.string().optional() })).optional(),
|
||||
remove_keys: z.array(z.object({ name: z.string().optional() })).optional(),
|
||||
updates: z
|
||||
.array(
|
||||
z.object({ key: z.string().optional(), value: z.string().optional() }),
|
||||
)
|
||||
.optional(),
|
||||
rename_keys: z
|
||||
.array(
|
||||
z.object({
|
||||
old_key: z.string().optional(),
|
||||
new_key: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
filter_values: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string().optional(),
|
||||
value: z.string().optional(),
|
||||
operator: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
...OutputSchema,
|
||||
export const VariableAssignerSchema = {
|
||||
variables: z.array(
|
||||
z.object({
|
||||
variable: z.string().optional(),
|
||||
operator: z.string().optional(),
|
||||
parameter: z.string().or(z.number()).optional(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export const FormSchema = z.object(RetrievalPartialSchema);
|
||||
export const FormSchema = z.object(VariableAssignerSchema);
|
||||
|
||||
export type DataOperationsFormSchemaType = z.infer<typeof FormSchema>;
|
||||
export type VariableAssignerFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const outputList = buildOutputList(initialDataOperationsValues.outputs);
|
||||
// const outputList = buildOutputList(initialVariableAssignerValues.outputs);
|
||||
|
||||
function VariableAssignerForm({ node }: INextOperatorForm) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultValues = useFormValues(initialDataOperationsValues, node);
|
||||
|
||||
const form = useForm<DataOperationsFormSchemaType>({
|
||||
const form = useForm<VariableAssignerFormSchemaType>({
|
||||
defaultValues: defaultValues,
|
||||
mode: 'onChange',
|
||||
resolver: zodResolver(FormSchema),
|
||||
shouldUnregister: true,
|
||||
});
|
||||
|
||||
const OperationsOptions = buildOptions(
|
||||
Operations,
|
||||
t,
|
||||
`flow.operationsOptions`,
|
||||
true,
|
||||
);
|
||||
|
||||
useWatchFormChange(node?.id, form, true);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<QueryVariableList
|
||||
tooltip={t('flow.queryTip')}
|
||||
label={t('flow.query')}
|
||||
types={[JsonSchemaDataType.Array, JsonSchemaDataType.Object]}
|
||||
></QueryVariableList>
|
||||
<Separator />
|
||||
<RAGFlowFormItem name="operations" label={t('flow.operations')}>
|
||||
<SelectWithSearch options={OperationsOptions} allowClear />
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<Output list={outputList} isFormRequired></Output>
|
||||
<DynamicVariables name="variables" label="Variables"></DynamicVariables>
|
||||
{/* <Output list={outputList} isFormRequired></Output> */}
|
||||
</FormWrapper>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { buildOptions } from '@/utils/form';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
JsonSchemaDataType,
|
||||
VariableAssignerLogicalArrayOperator,
|
||||
VariableAssignerLogicalNumberOperator,
|
||||
VariableAssignerLogicalOperator,
|
||||
} from '../../constant';
|
||||
|
||||
export function useBuildLogicalOptions() {
|
||||
const buildLogicalOptions = useCallback((type: string) => {
|
||||
if (
|
||||
type?.toLowerCase().startsWith(JsonSchemaDataType.Array.toLowerCase())
|
||||
) {
|
||||
return buildOptions(VariableAssignerLogicalArrayOperator);
|
||||
}
|
||||
|
||||
if (type === JsonSchemaDataType.Number) {
|
||||
return buildOptions(VariableAssignerLogicalNumberOperator);
|
||||
}
|
||||
|
||||
return buildOptions(VariableAssignerLogicalOperator);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
buildLogicalOptions,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue