mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-02 00:25:06 +08:00
### What problem does this PR solve? Feat: Display the pipeline on the agent canvas #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
107
web/src/pages/agent/form/extractor-form/index.tsx
Normal file
107
web/src/pages/agent/form/extractor-form/index.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { LargeModelFormField } from '@/components/large-model-form-field';
|
||||
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
|
||||
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 {
|
||||
ContextGeneratorFieldName,
|
||||
initialExtractorValues,
|
||||
} from '../../constant/pipeline';
|
||||
import { useBuildNodeOutputOptions } from '../../hooks/use-build-options';
|
||||
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 } from '../components/output';
|
||||
import { useSwitchPrompt } from './use-switch-prompt';
|
||||
|
||||
export const FormSchema = z.object({
|
||||
field_name: z.string(),
|
||||
sys_prompt: z.string(),
|
||||
prompts: z.string().optional(),
|
||||
...LlmSettingSchema,
|
||||
});
|
||||
|
||||
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const outputList = buildOutputList(initialExtractorValues.outputs);
|
||||
|
||||
const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
const defaultValues = useFormValues(initialExtractorValues, node);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<ExtractorFormSchemaType>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
// mode: 'onChange',
|
||||
});
|
||||
|
||||
const promptOptions = useBuildNodeOutputOptions(node?.id);
|
||||
|
||||
const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow');
|
||||
|
||||
const {
|
||||
handleFieldNameChange,
|
||||
confirmSwitch,
|
||||
hideModal,
|
||||
visible,
|
||||
cancelSwitch,
|
||||
} = useSwitchPrompt(form);
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<LargeModelFormField></LargeModelFormField>
|
||||
<RAGFlowFormItem label={t('dataflow.fieldName')} name="field_name">
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
handleFieldNameChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
placeholder={t('dataFlowPlaceholder')}
|
||||
options={options}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem label={t('flow.systemPrompt')} name="sys_prompt">
|
||||
<PromptEditor
|
||||
placeholder={t('flow.messagePlaceholder')}
|
||||
showToolbar={true}
|
||||
baseOptions={promptOptions}
|
||||
></PromptEditor>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem label={t('flow.userPrompt')} name="prompts">
|
||||
<PromptEditor
|
||||
showToolbar={true}
|
||||
baseOptions={promptOptions}
|
||||
></PromptEditor>
|
||||
</RAGFlowFormItem>
|
||||
<Output list={outputList}></Output>
|
||||
</FormWrapper>
|
||||
{visible && (
|
||||
<ConfirmDeleteDialog
|
||||
title={t('dataflow.switchPromptMessage')}
|
||||
open
|
||||
onOpenChange={hideModal}
|
||||
onOk={confirmSwitch}
|
||||
onCancel={cancelSwitch}
|
||||
></ConfirmDeleteDialog>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ExtractorForm);
|
||||
69
web/src/pages/agent/form/extractor-form/use-switch-prompt.ts
Normal file
69
web/src/pages/agent/form/extractor-form/use-switch-prompt.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FormSchema = z.object({
|
||||
field_name: z.string(),
|
||||
sys_prompt: z.string(),
|
||||
prompts: z.string().optional(),
|
||||
...LlmSettingSchema,
|
||||
});
|
||||
|
||||
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
export function useSwitchPrompt(form: UseFormReturn<ExtractorFormSchemaType>) {
|
||||
const { visible, showModal, hideModal } = useSetModalState();
|
||||
const { t } = useTranslation();
|
||||
const previousFieldNames = useRef<string[]>([form.getValues('field_name')]);
|
||||
|
||||
const setPromptValue = useCallback(
|
||||
(field: keyof ExtractorFormSchemaType, key: string, value: string) => {
|
||||
form.setValue(field, t(`dataflow.prompts.${key}.${value}`), {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
},
|
||||
[form, t],
|
||||
);
|
||||
|
||||
const handleFieldNameChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value) {
|
||||
const names = previousFieldNames.current;
|
||||
if (names.length > 1) {
|
||||
names.shift();
|
||||
}
|
||||
names.push(value);
|
||||
showModal();
|
||||
}
|
||||
},
|
||||
[showModal],
|
||||
);
|
||||
|
||||
const confirmSwitch = useCallback(() => {
|
||||
const value = form.getValues('field_name');
|
||||
setPromptValue('sys_prompt', 'system', value);
|
||||
setPromptValue('prompts', 'user', value);
|
||||
}, [form, setPromptValue]);
|
||||
|
||||
const cancelSwitch = useCallback(() => {
|
||||
const previousValue = previousFieldNames.current.at(-2);
|
||||
if (previousValue) {
|
||||
form.setValue('field_name', previousValue, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
return {
|
||||
handleFieldNameChange,
|
||||
confirmSwitch,
|
||||
hideModal,
|
||||
visible,
|
||||
cancelSwitch,
|
||||
};
|
||||
}
|
||||
191
web/src/pages/agent/form/hierarchical-merger-form/index.tsx
Normal file
191
web/src/pages/agent/form/hierarchical-merger-form/index.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { BlockButton, Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Form, FormLabel } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Hierarchy,
|
||||
initialHierarchicalMergerValues,
|
||||
} from '../../constant/pipeline';
|
||||
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 } from '../components/output';
|
||||
|
||||
const outputList = buildOutputList(initialHierarchicalMergerValues.outputs);
|
||||
|
||||
const HierarchyOptions = [
|
||||
{ label: 'H1', value: Hierarchy.H1 },
|
||||
{ label: 'H2', value: Hierarchy.H2 },
|
||||
{ label: 'H3', value: Hierarchy.H3 },
|
||||
{ label: 'H4', value: Hierarchy.H4 },
|
||||
{ label: 'H5', value: Hierarchy.H5 },
|
||||
];
|
||||
|
||||
export const FormSchema = z.object({
|
||||
hierarchy: z.string(),
|
||||
levels: z.array(
|
||||
z.object({
|
||||
expressions: z.array(
|
||||
z.object({
|
||||
expression: z.string().refine(
|
||||
(val) => {
|
||||
try {
|
||||
// Try converting the string to a RegExp
|
||||
new RegExp(val);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message: 'Must be a valid regular expression string',
|
||||
},
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type HierarchicalMergerFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
type RegularExpressionsProps = {
|
||||
index: number;
|
||||
parentName: string;
|
||||
removeParent: (index: number) => void;
|
||||
isLatest: boolean;
|
||||
};
|
||||
|
||||
export function RegularExpressions({
|
||||
index,
|
||||
parentName,
|
||||
isLatest,
|
||||
removeParent,
|
||||
}: RegularExpressionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
const name = `${parentName}.${index}.expressions`;
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row justify-between items-center">
|
||||
<span>H{index + 1}</span>
|
||||
{isLatest && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => removeParent(index)}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FormLabel required className="mb-2 text-text-secondary">
|
||||
{t('dataflow.regularExpressions')}
|
||||
</FormLabel>
|
||||
<section className="space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center gap-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<RAGFlowFormItem
|
||||
name={`${name}.${index}.expression`}
|
||||
label={'expression'}
|
||||
labelClassName="!hidden"
|
||||
>
|
||||
<Input className="!m-0"></Input>
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
{index === 0 ? (
|
||||
<Button
|
||||
onClick={() => append({ expression: '' })}
|
||||
variant={'ghost'}
|
||||
>
|
||||
<Plus></Plus>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const HierarchicalMergerForm = ({ node }: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultValues = useFormValues(initialHierarchicalMergerValues, node);
|
||||
|
||||
const form = useForm<HierarchicalMergerFormSchemaType>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const name = 'levels';
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<RAGFlowFormItem name={'hierarchy'} label={t('dataflow.hierarchy')}>
|
||||
<SelectWithSearch options={HierarchyOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center">
|
||||
<div className="flex-1">
|
||||
<RegularExpressions
|
||||
parentName={name}
|
||||
index={index}
|
||||
removeParent={remove}
|
||||
isLatest={index === fields.length - 1}
|
||||
></RegularExpressions>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{fields.length < 5 && (
|
||||
<BlockButton
|
||||
onClick={() => append({ expressions: [{ expression: '' }] })}
|
||||
>
|
||||
{t('common.add')}
|
||||
</BlockButton>
|
||||
)}
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(HierarchicalMergerForm);
|
||||
106
web/src/pages/agent/form/parser-form/common-form-fields.tsx
Normal file
106
web/src/pages/agent/form/parser-form/common-form-fields.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { crossLanguageOptions } from '@/components/cross-language-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import {
|
||||
LLMFormField,
|
||||
LLMFormFieldProps,
|
||||
} from '@/components/llm-setting-items/llm-form-field';
|
||||
import {
|
||||
SelectWithSearch,
|
||||
SelectWithSearchFlagOptionType,
|
||||
} from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { upperCase, upperFirst } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FileType,
|
||||
OutputFormatMap,
|
||||
SpreadsheetOutputFormat,
|
||||
} from '../../constant/pipeline';
|
||||
import { CommonProps } from './interface';
|
||||
import { buildFieldNameWithPrefix } from './utils';
|
||||
|
||||
const UppercaseFields = [
|
||||
SpreadsheetOutputFormat.Html,
|
||||
SpreadsheetOutputFormat.Json,
|
||||
];
|
||||
|
||||
function buildOutputOptionsFormatMap() {
|
||||
return Object.entries(OutputFormatMap).reduce<
|
||||
Record<string, SelectWithSearchFlagOptionType[]>
|
||||
>((pre, [key, value]) => {
|
||||
pre[key] = Object.values(value).map((v) => ({
|
||||
label: UppercaseFields.some((x) => x === v)
|
||||
? upperCase(v)
|
||||
: upperFirst(v),
|
||||
value: v,
|
||||
}));
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export type OutputFormatFormFieldProps = CommonProps & {
|
||||
fileType: FileType;
|
||||
};
|
||||
|
||||
export function OutputFormatFormField({
|
||||
prefix,
|
||||
fileType,
|
||||
}: OutputFormatFormFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={buildFieldNameWithPrefix(`output_format`, prefix)}
|
||||
label={t('dataflow.outputFormat')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={buildOutputOptionsFormatMap()[fileType]}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function ParserMethodFormField({
|
||||
prefix,
|
||||
optionsWithoutLLM,
|
||||
}: CommonProps & { optionsWithoutLLM?: { value: string; label: string }[] }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<LayoutRecognizeFormField
|
||||
name={buildFieldNameWithPrefix(`parse_method`, prefix)}
|
||||
horizontal={false}
|
||||
optionsWithoutLLM={optionsWithoutLLM}
|
||||
label={t('dataflow.parserMethod')}
|
||||
></LayoutRecognizeFormField>
|
||||
);
|
||||
}
|
||||
|
||||
export function LargeModelFormField({
|
||||
prefix,
|
||||
options,
|
||||
}: CommonProps & Pick<LLMFormFieldProps, 'options'>) {
|
||||
return (
|
||||
<LLMFormField
|
||||
name={buildFieldNameWithPrefix('llm_id', prefix)}
|
||||
options={options}
|
||||
></LLMFormField>
|
||||
);
|
||||
}
|
||||
|
||||
export function LanguageFormField({ prefix }: CommonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={buildFieldNameWithPrefix(`lang`, prefix)}
|
||||
label={t('dataflow.lang')}
|
||||
>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
options={crossLanguageOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
30
web/src/pages/agent/form/parser-form/email-form-fields.tsx
Normal file
30
web/src/pages/agent/form/parser-form/email-form-fields.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ParserFields } from '../../constant/pipeline';
|
||||
import { CommonProps } from './interface';
|
||||
import { buildFieldNameWithPrefix } from './utils';
|
||||
|
||||
const options = buildOptions(ParserFields);
|
||||
|
||||
export function EmailFormFields({ prefix }: CommonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name={buildFieldNameWithPrefix(`fields`, prefix)}
|
||||
label={t('dataflow.fields')}
|
||||
>
|
||||
{(field) => (
|
||||
<MultiSelect
|
||||
options={options}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
variant="inverted"
|
||||
></MultiSelect>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
60
web/src/pages/agent/form/parser-form/image-form-fields.tsx
Normal file
60
web/src/pages/agent/form/parser-form/image-form-fields.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ImageParseMethod } from '../../constant/pipeline';
|
||||
import { LanguageFormField, ParserMethodFormField } from './common-form-fields';
|
||||
import { CommonProps } from './interface';
|
||||
import { useSetInitialLanguage } from './use-set-initial-language';
|
||||
import { buildFieldNameWithPrefix } from './utils';
|
||||
|
||||
export function ImageFormFields({ prefix }: CommonProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const options = buildOptions(
|
||||
ImageParseMethod,
|
||||
t,
|
||||
'dataflow.imageParseMethodOptions',
|
||||
);
|
||||
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
|
||||
|
||||
const parseMethod = useWatch({
|
||||
name: parseMethodName,
|
||||
});
|
||||
|
||||
const languageShown = useMemo(() => {
|
||||
return !isEmpty(parseMethod) && parseMethod !== ImageParseMethod.OCR;
|
||||
}, [parseMethod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(form.getValues(parseMethodName))) {
|
||||
form.setValue(parseMethodName, ImageParseMethod.OCR, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}, [form, parseMethodName]);
|
||||
|
||||
useSetInitialLanguage({ prefix, languageShown });
|
||||
|
||||
return (
|
||||
<>
|
||||
<ParserMethodFormField
|
||||
prefix={prefix}
|
||||
optionsWithoutLLM={options}
|
||||
></ParserMethodFormField>
|
||||
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
|
||||
{languageShown && (
|
||||
<RAGFlowFormItem
|
||||
name={buildFieldNameWithPrefix('system_prompt', prefix)}
|
||||
label={t('dataflow.systemPrompt')}
|
||||
>
|
||||
<Textarea placeholder={t('dataflow.systemPromptPlaceholder')} />
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
226
web/src/pages/agent/form/parser-form/index.tsx
Normal file
226
web/src/pages/agent/form/parser-form/index.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
import {
|
||||
SelectWithSearch,
|
||||
SelectWithSearchFlagOptionType,
|
||||
} from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { BlockButton, Button } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useHover } from 'ahooks';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
UseFieldArrayRemove,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
FileType,
|
||||
InitialOutputFormatMap,
|
||||
initialParserValues,
|
||||
} from '../../constant/pipeline';
|
||||
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 { Output } from '../components/output';
|
||||
import { OutputFormatFormField } from './common-form-fields';
|
||||
import { EmailFormFields } from './email-form-fields';
|
||||
import { ImageFormFields } from './image-form-fields';
|
||||
import { PdfFormFields } from './pdf-form-fields';
|
||||
import { buildFieldNameWithPrefix } from './utils';
|
||||
import { VideoFormFields } from './video-form-fields';
|
||||
|
||||
const outputList = buildOutputList(initialParserValues.outputs);
|
||||
|
||||
const FileFormatWidgetMap = {
|
||||
[FileType.PDF]: PdfFormFields,
|
||||
[FileType.Video]: VideoFormFields,
|
||||
[FileType.Audio]: VideoFormFields,
|
||||
[FileType.Email]: EmailFormFields,
|
||||
[FileType.Image]: ImageFormFields,
|
||||
};
|
||||
|
||||
type ParserItemProps = {
|
||||
name: string;
|
||||
index: number;
|
||||
fieldLength: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
fileFormatOptions: SelectWithSearchFlagOptionType[];
|
||||
};
|
||||
|
||||
export const FormSchema = z.object({
|
||||
setups: z.array(
|
||||
z.object({
|
||||
fileFormat: z.string().nullish(),
|
||||
output_format: z.string().optional(),
|
||||
parse_method: z.string().optional(),
|
||||
lang: z.string().optional(),
|
||||
fields: z.array(z.string()).optional(),
|
||||
llm_id: z.string().optional(),
|
||||
system_prompt: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type ParserFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
function ParserItem({
|
||||
name,
|
||||
index,
|
||||
fieldLength,
|
||||
remove,
|
||||
fileFormatOptions,
|
||||
}: ParserItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext<ParserFormSchemaType>();
|
||||
const ref = useRef(null);
|
||||
const isHovering = useHover(ref);
|
||||
|
||||
const prefix = `${name}.${index}`;
|
||||
const fileFormat = form.getValues(`setups.${index}.fileFormat`);
|
||||
|
||||
const values = form.getValues();
|
||||
const parserList = values.setups.slice(); // Adding, deleting, or modifying the parser array will not change the reference.
|
||||
|
||||
const filteredFileFormatOptions = useMemo(() => {
|
||||
const otherFileFormatList = parserList
|
||||
.filter((_, idx) => idx !== index)
|
||||
.map((x) => x.fileFormat);
|
||||
|
||||
return fileFormatOptions.filter((x) => {
|
||||
return !otherFileFormatList.includes(x.value);
|
||||
});
|
||||
}, [fileFormatOptions, index, parserList]);
|
||||
|
||||
const Widget =
|
||||
typeof fileFormat === 'string' && fileFormat in FileFormatWidgetMap
|
||||
? FileFormatWidgetMap[fileFormat as keyof typeof FileFormatWidgetMap]
|
||||
: () => <></>;
|
||||
|
||||
const handleFileTypeChange = useCallback(
|
||||
(value: FileType) => {
|
||||
form.setValue(
|
||||
`setups.${index}.output_format`,
|
||||
InitialOutputFormatMap[value],
|
||||
{ shouldDirty: true, shouldValidate: true, shouldTouch: true },
|
||||
);
|
||||
},
|
||||
[form, index],
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn('space-y-5 py-2.5 rounded-md', {
|
||||
'bg-state-error-5': isHovering,
|
||||
})}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-text-primary text-sm font-medium">
|
||||
Parser {index + 1}
|
||||
</span>
|
||||
{index > 0 && (
|
||||
<Button variant={'ghost'} onClick={() => remove(index)} ref={ref}>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<RAGFlowFormItem
|
||||
name={buildFieldNameWithPrefix(`fileFormat`, prefix)}
|
||||
label={t('dataflow.fileFormats')}
|
||||
>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
value={field.value}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
handleFileTypeChange(val as FileType);
|
||||
}}
|
||||
options={filteredFileFormatOptions}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<Widget prefix={prefix} fileType={fileFormat as FileType}></Widget>
|
||||
<div className="hidden">
|
||||
<OutputFormatFormField
|
||||
prefix={prefix}
|
||||
fileType={fileFormat as FileType}
|
||||
/>
|
||||
</div>
|
||||
{index < fieldLength - 1 && <Separator />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const ParserForm = ({ node }: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultValues = useFormValues(initialParserValues, node);
|
||||
|
||||
const FileFormatOptions = buildOptions(
|
||||
FileType,
|
||||
t,
|
||||
'dataflow.fileFormatOptions',
|
||||
).filter(
|
||||
(x) => x.value !== FileType.Video, // Temporarily hide the video option
|
||||
);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
shouldUnregister: true,
|
||||
});
|
||||
|
||||
const name = 'setups';
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const add = useCallback(() => {
|
||||
append({
|
||||
fileFormat: null,
|
||||
output_format: '',
|
||||
parse_method: '',
|
||||
lang: '',
|
||||
fields: [],
|
||||
llm_id: '',
|
||||
});
|
||||
}, [append]);
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="px-5">
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<ParserItem
|
||||
key={field.id}
|
||||
name={name}
|
||||
index={index}
|
||||
fieldLength={fields.length}
|
||||
remove={remove}
|
||||
fileFormatOptions={FileFormatOptions}
|
||||
></ParserItem>
|
||||
);
|
||||
})}
|
||||
{fields.length < FileFormatOptions.length && (
|
||||
<BlockButton onClick={add} type="button" className="mt-2.5">
|
||||
{t('dataflow.addParser')}
|
||||
</BlockButton>
|
||||
)}
|
||||
</form>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParserForm);
|
||||
3
web/src/pages/agent/form/parser-form/interface.ts
Normal file
3
web/src/pages/agent/form/parser-form/interface.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type CommonProps = {
|
||||
prefix: string;
|
||||
};
|
||||
44
web/src/pages/agent/form/parser-form/pdf-form-fields.tsx
Normal file
44
web/src/pages/agent/form/parser-form/pdf-form-fields.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { LanguageFormField, ParserMethodFormField } from './common-form-fields';
|
||||
import { CommonProps } from './interface';
|
||||
import { useSetInitialLanguage } from './use-set-initial-language';
|
||||
import { buildFieldNameWithPrefix } from './utils';
|
||||
|
||||
export function PdfFormFields({ prefix }: CommonProps) {
|
||||
const form = useFormContext();
|
||||
|
||||
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
|
||||
|
||||
const parseMethod = useWatch({
|
||||
name: parseMethodName,
|
||||
});
|
||||
|
||||
const languageShown = useMemo(() => {
|
||||
return (
|
||||
!isEmpty(parseMethod) &&
|
||||
parseMethod !== ParseDocumentType.DeepDOC &&
|
||||
parseMethod !== ParseDocumentType.PlainText
|
||||
);
|
||||
}, [parseMethod]);
|
||||
|
||||
useSetInitialLanguage({ prefix, languageShown });
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(form.getValues(parseMethodName))) {
|
||||
form.setValue(parseMethodName, ParseDocumentType.DeepDOC, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}, [form, parseMethodName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ParserMethodFormField prefix={prefix}></ParserMethodFormField>
|
||||
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { crossLanguageOptions } from '@/components/cross-language-form-field';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { buildFieldNameWithPrefix } from './utils';
|
||||
|
||||
export function useSetInitialLanguage({
|
||||
prefix,
|
||||
languageShown,
|
||||
}: {
|
||||
prefix: string;
|
||||
languageShown: boolean;
|
||||
}) {
|
||||
const form = useFormContext();
|
||||
const lang = form.getValues(buildFieldNameWithPrefix('lang', prefix));
|
||||
|
||||
useEffect(() => {
|
||||
if (languageShown && isEmpty(lang)) {
|
||||
form.setValue(
|
||||
buildFieldNameWithPrefix('lang', prefix),
|
||||
crossLanguageOptions[0].value,
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [form, lang, languageShown, prefix]);
|
||||
}
|
||||
3
web/src/pages/agent/form/parser-form/utils.ts
Normal file
3
web/src/pages/agent/form/parser-form/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function buildFieldNameWithPrefix(name: string, prefix: string) {
|
||||
return `${prefix}.${name}`;
|
||||
}
|
||||
22
web/src/pages/agent/form/parser-form/video-form-fields.tsx
Normal file
22
web/src/pages/agent/form/parser-form/video-form-fields.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
||||
import {
|
||||
LargeModelFormField,
|
||||
OutputFormatFormFieldProps,
|
||||
} from './common-form-fields';
|
||||
|
||||
export function VideoFormFields({ prefix }: OutputFormatFormFieldProps) {
|
||||
const modelOptions = useComposeLlmOptionsByModelTypes([
|
||||
LlmModelType.Speech2text,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Multimodal Model */}
|
||||
<LargeModelFormField
|
||||
prefix={prefix}
|
||||
options={modelOptions}
|
||||
></LargeModelFormField>
|
||||
</>
|
||||
);
|
||||
}
|
||||
101
web/src/pages/agent/form/splitter-form/index.tsx
Normal file
101
web/src/pages/agent/form/splitter-form/index.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { DelimiterInput } from '@/components/delimiter-form-field';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||
import { BlockButton, Button } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { initialSplitterValues } from '../../constant/pipeline';
|
||||
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 } from '../components/output';
|
||||
|
||||
const outputList = buildOutputList(initialSplitterValues.outputs);
|
||||
|
||||
export const FormSchema = z.object({
|
||||
chunk_token_size: z.number(),
|
||||
delimiters: z.array(
|
||||
z.object({
|
||||
value: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
overlapped_percent: z.number(), // 0.0 - 0.3 , 0% - 30%
|
||||
});
|
||||
|
||||
export type SplitterFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const SplitterForm = ({ node }: INextOperatorForm) => {
|
||||
const defaultValues = useFormValues(initialSplitterValues, node);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<SplitterFormSchemaType>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
const name = 'delimiters';
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<SliderInputFormField
|
||||
name="chunk_token_size"
|
||||
max={2048}
|
||||
label={t('knowledgeConfiguration.chunkTokenNumber')}
|
||||
></SliderInputFormField>
|
||||
<SliderInputFormField
|
||||
name="overlapped_percent"
|
||||
max={30}
|
||||
min={0}
|
||||
label={t('dataflow.overlappedPercent')}
|
||||
></SliderInputFormField>
|
||||
<section>
|
||||
<span className="mb-2 inline-block">{t('flow.delimiters')}</span>
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center gap-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<RAGFlowFormItem
|
||||
name={`${name}.${index}.value`}
|
||||
label="delimiter"
|
||||
labelClassName="!hidden"
|
||||
>
|
||||
<DelimiterInput className="!m-0"></DelimiterInput>
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<BlockButton onClick={() => append({ value: '\n' })}>
|
||||
{t('common.add')}
|
||||
</BlockButton>
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SplitterForm);
|
||||
91
web/src/pages/agent/form/tokenizer-form/index.tsx
Normal file
91
web/src/pages/agent/form/tokenizer-form/index.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
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 {
|
||||
initialTokenizerValues,
|
||||
TokenizerFields,
|
||||
TokenizerSearchMethod,
|
||||
} 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 } from '../components/output';
|
||||
|
||||
const outputList = buildOutputList(initialTokenizerValues.outputs);
|
||||
|
||||
export const FormSchema = z.object({
|
||||
search_method: z.array(z.string()).min(1),
|
||||
filename_embd_weight: z.number(),
|
||||
fields: z.string(),
|
||||
});
|
||||
|
||||
export type TokenizerFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultValues = useFormValues(initialTokenizerValues, node);
|
||||
|
||||
const SearchMethodOptions = buildOptions(
|
||||
TokenizerSearchMethod,
|
||||
t,
|
||||
`dataflow.tokenizerSearchMethodOptions`,
|
||||
);
|
||||
const FieldsOptions = buildOptions(
|
||||
TokenizerFields,
|
||||
t,
|
||||
'dataflow.tokenizerFieldsOptions',
|
||||
);
|
||||
|
||||
const form = useForm<TokenizerFormSchemaType>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<RAGFlowFormItem
|
||||
name="search_method"
|
||||
label={t('dataflow.searchMethod')}
|
||||
tooltip={t('dataflow.searchMethodTip')}
|
||||
>
|
||||
{(field) => (
|
||||
<MultiSelect
|
||||
options={SearchMethodOptions}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
variant="inverted"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<SliderInputFormField
|
||||
name="filename_embd_weight"
|
||||
label={t('dataflow.filenameEmbeddingWeight')}
|
||||
max={0.5}
|
||||
step={0.01}
|
||||
></SliderInputFormField>
|
||||
<RAGFlowFormItem name="fields" label={t('dataflow.fields')}>
|
||||
{(field) => <SelectWithSearch options={FieldsOptions} {...field} />}
|
||||
</RAGFlowFormItem>
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TokenizerForm);
|
||||
Reference in New Issue
Block a user