mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-22 07:31:05 +08:00
Feat: Configure the relevant pipeline parameters on the dataset configuration page. (#17100)
This commit is contained in:
@@ -2,34 +2,45 @@ import { FormLayout } from '@/constants/form';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { SliderInputFormField } from './slider-input-form-field';
|
||||
|
||||
export function AutoKeywordsFormField() {
|
||||
interface AutoFieldProps {
|
||||
name?: string;
|
||||
layout?: FormLayout;
|
||||
}
|
||||
|
||||
export function AutoKeywordsFormField({
|
||||
name = 'parser_config.auto_keywords',
|
||||
layout = FormLayout.Vertical,
|
||||
}: AutoFieldProps) {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<SliderInputFormField
|
||||
name={'parser_config.auto_keywords'}
|
||||
name={name}
|
||||
label={t('autoKeywords')}
|
||||
max={30}
|
||||
min={0}
|
||||
tooltip={t('autoKeywordsTip')}
|
||||
layout={FormLayout.Horizontal}
|
||||
layout={layout}
|
||||
sliderTestId="ds-settings-parser-auto-keyword-slider"
|
||||
numberInputTestId="ds-settings-parser-auto-keyword-input"
|
||||
></SliderInputFormField>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoQuestionsFormField() {
|
||||
export function AutoQuestionsFormField({
|
||||
name = 'parser_config.auto_questions',
|
||||
layout = FormLayout.Vertical,
|
||||
}: AutoFieldProps) {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<SliderInputFormField
|
||||
name={'parser_config.auto_questions'}
|
||||
name={name}
|
||||
label={t('autoQuestions')}
|
||||
max={10}
|
||||
min={0}
|
||||
tooltip={t('autoQuestionsTip')}
|
||||
layout={FormLayout.Horizontal}
|
||||
layout={layout}
|
||||
sliderTestId="ds-settings-parser-auto-question-slider"
|
||||
numberInputTestId="ds-settings-parser-auto-question-input"
|
||||
></SliderInputFormField>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IAgentLogResponse,
|
||||
IAgentLogsRequest,
|
||||
IAgentLogsResponse,
|
||||
IBuiltinPipelineListResponse,
|
||||
IFlow,
|
||||
IFlowTemplate,
|
||||
IPipeLineListRequest,
|
||||
@@ -77,6 +78,9 @@ export const enum AgentApiAction {
|
||||
FetchSharedAgent = 'fetchSharedAgent',
|
||||
FetchAgentTags = 'fetchAgentTags',
|
||||
UpdateAgentTags = 'updateAgentTags',
|
||||
FetchPipelineNodes = 'fetchPipelineNodes',
|
||||
FetchBuiltinPipelineList = 'fetchBuiltinPipelineList',
|
||||
FetchBuiltinPipelineDetail = 'fetchBuiltinPipelineDetail',
|
||||
}
|
||||
|
||||
export const useFetchAgentTemplates = () => {
|
||||
@@ -867,6 +871,37 @@ export const useFetchAgentList = ({
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const BuiltinPipelineKeys = {
|
||||
list: (type: string) =>
|
||||
[AgentApiAction.FetchBuiltinPipelineList, type] as const,
|
||||
detail: (id: string) =>
|
||||
[AgentApiAction.FetchBuiltinPipelineDetail, id] as const,
|
||||
};
|
||||
|
||||
export const useFetchBuiltinPipelines = (type = 'builtin', enabled = true) => {
|
||||
const { data, isFetching: loading } = useQuery<IBuiltinPipelineListResponse>({
|
||||
queryKey: BuiltinPipelineKeys.list(type),
|
||||
initialData: { canvas: [], total: 0 },
|
||||
gcTime: 0,
|
||||
enabled,
|
||||
queryFn: async () => {
|
||||
const { data } = await agentService.listBuiltinPipelines(
|
||||
{ params: { type } },
|
||||
true,
|
||||
);
|
||||
return data?.data ?? { canvas: [], total: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
const options =
|
||||
data?.canvas?.map((item) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
})) ?? [];
|
||||
|
||||
return { data, loading, options };
|
||||
};
|
||||
|
||||
export const useCancelDataflow = () => {
|
||||
const {
|
||||
data,
|
||||
@@ -1116,3 +1151,26 @@ export const useExportAgentLog = () => {
|
||||
|
||||
return { exportLogs: mutateAsync, loading };
|
||||
};
|
||||
|
||||
export const useFetchPipelineDslByPipelineId = (
|
||||
pipelineId?: string,
|
||||
isBuiltin = false,
|
||||
) => {
|
||||
const { data: dsl, isFetching: loading } = useQuery({
|
||||
queryKey: isBuiltin
|
||||
? BuiltinPipelineKeys.detail(pipelineId!)
|
||||
: [AgentApiAction.FetchPipelineNodes, pipelineId],
|
||||
initialData: {},
|
||||
gcTime: 0,
|
||||
enabled: !!pipelineId,
|
||||
queryFn: async () => {
|
||||
const { data } = isBuiltin
|
||||
? await agentService.getBuiltinPipeline(pipelineId!)
|
||||
: await agentService.getAgent(pipelineId!);
|
||||
const flow = data?.data;
|
||||
return flow?.dsl ?? {};
|
||||
},
|
||||
});
|
||||
|
||||
return { dsl, loading };
|
||||
};
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { IFlow } from '@/interfaces/database/agent';
|
||||
import dataflowService from '@/services/dataflow-service';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
export const enum DataflowApiAction {
|
||||
ListDataflow = 'listDataflow',
|
||||
RemoveDataflow = 'removeDataflow',
|
||||
FetchDataflow = 'fetchDataflow',
|
||||
RunDataflow = 'runDataflow',
|
||||
SetDataflow = 'setDataflow',
|
||||
}
|
||||
|
||||
export const useRemoveDataflow = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
data,
|
||||
isPending: loading,
|
||||
mutateAsync,
|
||||
} = useMutation({
|
||||
mutationKey: [DataflowApiAction.RemoveDataflow],
|
||||
mutationFn: async (ids: string[]) => {
|
||||
const { data } = await dataflowService.removeDataflow({
|
||||
canvas_ids: ids,
|
||||
});
|
||||
if (data.code === 0) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [DataflowApiAction.ListDataflow],
|
||||
});
|
||||
|
||||
message.success(t('message.deleted'));
|
||||
}
|
||||
return data.code;
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading, removeDataflow: mutateAsync };
|
||||
};
|
||||
|
||||
export const useSetDataflow = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
data,
|
||||
isPending: loading,
|
||||
mutateAsync,
|
||||
} = useMutation({
|
||||
mutationKey: [DataflowApiAction.SetDataflow],
|
||||
mutationFn: async (params: Partial<IFlow>) => {
|
||||
const { data } = await dataflowService.setDataflow(params);
|
||||
if (data.code === 0) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [DataflowApiAction.FetchDataflow],
|
||||
});
|
||||
|
||||
message.success(t(`message.${params.id ? 'modified' : 'created'}`));
|
||||
}
|
||||
return data?.code;
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading, setDataflow: mutateAsync };
|
||||
};
|
||||
|
||||
export const useFetchDataflow = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useQuery<IFlow>({
|
||||
queryKey: [DataflowApiAction.FetchDataflow, id],
|
||||
gcTime: 0,
|
||||
initialData: {} as IFlow,
|
||||
enabled: !!id,
|
||||
refetchOnWindowFocus: false,
|
||||
queryFn: async () => {
|
||||
const { data } = await dataflowService.fetchDataflow(id);
|
||||
|
||||
return data?.data ?? ({} as IFlow);
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading, refetch };
|
||||
};
|
||||
@@ -66,6 +66,7 @@ export const enum KnowledgeApiAction {
|
||||
DeleteKnowledge = 'deleteKnowledge',
|
||||
SaveKnowledge = 'saveKnowledge',
|
||||
FetchKnowledgeDetail = 'fetchKnowledgeDetail',
|
||||
FetchDatasetPipelineConfiguration = 'fetchDatasetPipelineConfiguration',
|
||||
FetchKnowledgeGraph = 'fetchKnowledgeGraph',
|
||||
FetchArtifactList = 'fetchArtifactList',
|
||||
FetchArtifactTopicList = 'fetchArtifactTopicList',
|
||||
@@ -264,6 +265,15 @@ export const useDeleteKnowledge = () => {
|
||||
return { data, loading, deleteKnowledge: mutateAsync };
|
||||
};
|
||||
|
||||
function isPipelineParserConfig(
|
||||
parserConfig: Record<string, any> | undefined,
|
||||
): boolean {
|
||||
if (!parserConfig || typeof parserConfig !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(parserConfig).some((key) => key.includes(':'));
|
||||
}
|
||||
|
||||
export const useUpdateKnowledge = (shouldFetchList = false) => {
|
||||
const knowledgeBaseId = useKnowledgeBaseId();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -308,7 +318,9 @@ export const useUpdateKnowledge = (shouldFetchList = false) => {
|
||||
description,
|
||||
permission,
|
||||
pagerank,
|
||||
parser_config: extractParserConfigExt(parser_config),
|
||||
parser_config: isPipelineParserConfig(parser_config)
|
||||
? parser_config
|
||||
: extractParserConfigExt(parser_config),
|
||||
...omit(ext, ['kb_id']),
|
||||
};
|
||||
|
||||
@@ -352,6 +364,36 @@ export const useFetchKnowledgeBaseConfiguration = (props?: {
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const DatasetPipelineConfigurationKeys = {
|
||||
detail: (knowledgeBaseId: string | null | undefined) =>
|
||||
[
|
||||
KnowledgeApiAction.FetchDatasetPipelineConfiguration,
|
||||
knowledgeBaseId,
|
||||
] as const,
|
||||
};
|
||||
|
||||
export const useFetchDatasetPipelineConfiguration = (props?: {
|
||||
isEdit?: boolean;
|
||||
}) => {
|
||||
const { isEdit = true } = props || { isEdit: true };
|
||||
const { id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const knowledgeBaseId = searchParams.get('id') || id;
|
||||
|
||||
const { data, isFetching: loading } = useQuery<IDataset>({
|
||||
queryKey: DatasetPipelineConfigurationKeys.detail(knowledgeBaseId),
|
||||
initialData: {} as IDataset,
|
||||
gcTime: 0,
|
||||
enabled: !!knowledgeBaseId && isEdit,
|
||||
queryFn: async () => {
|
||||
const { data } = await getKbDetail(knowledgeBaseId || '');
|
||||
return data?.data ?? {};
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const ArtifactKeys = {
|
||||
list: (
|
||||
datasetId: string,
|
||||
|
||||
@@ -199,6 +199,7 @@ export type BaseNodeData<TForm = any> = {
|
||||
name: string; // operator name
|
||||
color?: string;
|
||||
form?: TForm;
|
||||
operatorId?: string;
|
||||
};
|
||||
|
||||
export type BaseNode<T = any> = Node<BaseNodeData<T>>;
|
||||
@@ -301,6 +302,18 @@ export interface IPipeLineListRequest {
|
||||
ext?: string;
|
||||
}
|
||||
|
||||
export interface IBuiltinPipeline {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface IBuiltinPipelineListResponse {
|
||||
canvas: IBuiltinPipeline[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface GlobalVariableType {
|
||||
name: string;
|
||||
value: any;
|
||||
|
||||
@@ -499,6 +499,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
|
||||
testing: 'Retrieval testing',
|
||||
files: 'files',
|
||||
configuration: 'Configuration',
|
||||
nextConfiguration: 'Next configuration',
|
||||
knowledgeGraph: 'Knowledge graph',
|
||||
compilation: 'Compilation',
|
||||
export: 'Export',
|
||||
|
||||
@@ -445,6 +445,7 @@ export default {
|
||||
dataset: '知识库',
|
||||
testing: '检索测试',
|
||||
configuration: '配置',
|
||||
nextConfiguration: '下个配置',
|
||||
knowledgeGraph: '知识图谱',
|
||||
compilation: '编译',
|
||||
export: '导出',
|
||||
|
||||
@@ -351,6 +351,8 @@ export const initialTitleChunkerValues = {
|
||||
export const initialExtractorValues = {
|
||||
...initialLlmBaseValues,
|
||||
field_name: ContextGeneratorFieldName.Summary,
|
||||
auto_keywords: 0,
|
||||
auto_questions: 0,
|
||||
outputs: {
|
||||
chunks: { type: 'Array<Object>', value: [] },
|
||||
},
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { LargeModelFormField } from '@/components/large-model-form-field';
|
||||
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||
@@ -15,10 +19,11 @@ import {
|
||||
ContextGeneratorFieldName,
|
||||
initialExtractorValues,
|
||||
} from '../../constant/pipeline';
|
||||
import { useOwnerTenantId } from '../../context';
|
||||
import { useBuildNodeOutputOptions } from '../../hooks/use-build-options';
|
||||
import { useFormChangeCallback } from '../../hooks/use-form-change-callback';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { useOwnerTenantId } from '../../context';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { buildOutputList } from '../../utils/build-output-list';
|
||||
import { FormWrapper } from '../components/form-wrapper';
|
||||
@@ -29,6 +34,8 @@ export const FormSchema = z.object({
|
||||
field_name: z.string(),
|
||||
sys_prompt: z.string(),
|
||||
prompts: z.string().optional(),
|
||||
auto_keywords: z.number().optional(),
|
||||
auto_questions: z.number().optional(),
|
||||
...LlmSettingSchema,
|
||||
});
|
||||
|
||||
@@ -36,7 +43,11 @@ export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const outputList = buildOutputList(initialExtractorValues.outputs);
|
||||
|
||||
const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
const ExtractorForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
hideOutputs,
|
||||
}: INextOperatorForm) => {
|
||||
const defaultValues = useFormValues(initialExtractorValues, node);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -59,6 +70,7 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
} = useSwitchPrompt(form);
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
useFormChangeCallback(form, onValuesChange);
|
||||
|
||||
const ownerTenantId = useOwnerTenantId();
|
||||
const isToc = form.getValues('field_name') === 'toc';
|
||||
@@ -66,7 +78,11 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<LargeModelFormField ownerTenantId={ownerTenantId}></LargeModelFormField>
|
||||
<LargeModelFormField
|
||||
ownerTenantId={ownerTenantId}
|
||||
></LargeModelFormField>
|
||||
<AutoKeywordsFormField name="auto_keywords"></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField name="auto_questions"></AutoQuestionsFormField>
|
||||
<RAGFlowFormItem label={t('flow.fieldName')} name="field_name">
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
@@ -101,7 +117,7 @@ const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||
></PromptEditor>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<Output list={outputList}></Output>
|
||||
{!hideOutputs && <Output list={outputList}></Output>}
|
||||
</FormWrapper>
|
||||
{visible && (
|
||||
<ConfirmDeleteDialog
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
InitialOutputFormatMap,
|
||||
initialParserValues,
|
||||
} from '../../constant/pipeline';
|
||||
import { useFormChangeCallback } from '../../hooks/use-form-change-callback';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
@@ -326,7 +327,11 @@ function ParserItem({
|
||||
);
|
||||
}
|
||||
|
||||
const ParserForm = ({ node }: INextOperatorForm) => {
|
||||
const ParserForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
hideOutputs,
|
||||
}: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultValues = useFormValues(initialParserValues, node);
|
||||
|
||||
@@ -360,6 +365,7 @@ const ParserForm = ({ node }: INextOperatorForm) => {
|
||||
}, [append]);
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
useFormChangeCallback(form, onValuesChange);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -382,9 +388,11 @@ const ParserForm = ({ node }: INextOperatorForm) => {
|
||||
</BlockButton>
|
||||
)}
|
||||
</form>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
{!hideOutputs && (
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Switch } from '@/components/ui/switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ChevronDown, ChevronUp, Trash2 } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import {
|
||||
useFieldArray,
|
||||
useForm,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
initialTitleChunkerValues,
|
||||
TitleChunkerMethod,
|
||||
} from '../../constant/pipeline';
|
||||
import { useFormChangeCallback } from '../../hooks/use-form-change-callback';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
@@ -91,6 +92,10 @@ function LevelItem({
|
||||
|
||||
const name = `${parentName}.${index}.expression`;
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
removeParent(index);
|
||||
}, [removeParent, index]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1">
|
||||
@@ -108,7 +113,7 @@ function LevelItem({
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
size="sm"
|
||||
onClick={() => removeParent(index)}
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -137,6 +142,10 @@ function CardBody({ cardName }: CardBodyProps) {
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const handleAppendLevel = useCallback(() => {
|
||||
appendLevel({ expression: '' });
|
||||
}, [appendLevel]);
|
||||
|
||||
return (
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-4">
|
||||
@@ -151,10 +160,7 @@ function CardBody({ cardName }: CardBodyProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<BlockButton
|
||||
onClick={() => appendLevel({ expression: '' })}
|
||||
className="mt-4"
|
||||
>
|
||||
<BlockButton type="button" onClick={handleAppendLevel} className="mt-4">
|
||||
{t('flow.addRegularExpressions')}
|
||||
</BlockButton>
|
||||
</CardContent>
|
||||
@@ -173,46 +179,53 @@ function RulesFieldArray({ name }: RulesFieldArrayProps) {
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const handleAppendRule = useCallback(() => {
|
||||
append({
|
||||
levels: [{ expression: '' }],
|
||||
});
|
||||
}, [append]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{fields.map((cardField, cardIndex) => (
|
||||
<Card key={cardField.id}>
|
||||
<CardHeader className="flex flex-row justify-between items-center py-3 px-4 border-b bg-muted/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">
|
||||
{t('flow.rule', 'Rule')} {cardIndex + 1}
|
||||
</span>
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
size="sm"
|
||||
onClick={() => remove(cardIndex)}
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody cardIndex={cardIndex} cardName={`${name}.${cardIndex}`} />
|
||||
</Card>
|
||||
))}
|
||||
<BlockButton
|
||||
onClick={() =>
|
||||
append({
|
||||
levels: [{ expression: '' }],
|
||||
})
|
||||
}
|
||||
className="mt-4"
|
||||
>
|
||||
{fields.map((cardField, cardIndex) => {
|
||||
const handleRemoveCard = () => remove(cardIndex);
|
||||
|
||||
return (
|
||||
<Card key={cardField.id}>
|
||||
<CardHeader className="flex flex-row justify-between items-center py-3 px-4 border-b bg-muted/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">
|
||||
{t('flow.rule', 'Rule')} {cardIndex + 1}
|
||||
</span>
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
size="sm"
|
||||
onClick={handleRemoveCard}
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody cardIndex={cardIndex} cardName={`${name}.${cardIndex}`} />
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<BlockButton type="button" onClick={handleAppendRule} className="mt-4">
|
||||
{t('flow.addRule', 'Add Rule')}
|
||||
</BlockButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TitleChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
const TitleChunkerForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
hideOutputs,
|
||||
}: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const initialValues = useFormValues(initialTitleChunkerValues, node);
|
||||
|
||||
@@ -233,6 +246,11 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
const hierarchyOptions = useDynamicHierarchyOptions(form, activeRulesName);
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
useFormChangeCallback(form, onValuesChange);
|
||||
|
||||
const handleToggleShowAllTip = useCallback(() => {
|
||||
setShowAllTip((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -257,7 +275,7 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
/>
|
||||
<div
|
||||
className={`text-xs text-text-secondary w-full cursor-pointer `}
|
||||
onClick={() => setShowAllTip(!showAllTip)}
|
||||
onClick={handleToggleShowAllTip}
|
||||
>
|
||||
<div className={cn('flex justify-start items-start')}>
|
||||
<div
|
||||
@@ -306,9 +324,7 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
{(field) => (
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange?.(checked);
|
||||
}}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
@@ -327,9 +343,7 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
{(field) => (
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange?.(checked);
|
||||
}}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
@@ -349,9 +363,11 @@ const TitleChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
</div>
|
||||
{/* )} */}
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
{!hideOutputs && (
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { initialTokenChunkerValues } from '../../constant/pipeline';
|
||||
import { useFormChangeCallback } from '../../hooks/use-form-change-callback';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
@@ -41,7 +42,11 @@ export const FormSchema = z.object({
|
||||
|
||||
export type TokenChunkerFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const TokenChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
const TokenChunkerForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
hideOutputs,
|
||||
}: INextOperatorForm) => {
|
||||
const defaultValues = useFormValues(initialTokenChunkerValues, node);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -69,6 +74,7 @@ const TokenChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
useFormChangeCallback(form, onValuesChange);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -136,7 +142,7 @@ const TokenChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<BlockButton onClick={() => append({ value: '\n' })}>
|
||||
<BlockButton type="button" onClick={() => append({ value: '\n' })}>
|
||||
{t('common.add')}
|
||||
</BlockButton>
|
||||
</>
|
||||
@@ -211,9 +217,11 @@ const TokenChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
</fieldset>
|
||||
)}
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
{!hideOutputs && (
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TokenizerFields,
|
||||
TokenizerSearchMethod,
|
||||
} from '../../constant';
|
||||
import { useFormChangeCallback } from '../../hooks/use-form-change-callback';
|
||||
import { useFormValues } from '../../hooks/use-form-values';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
@@ -31,7 +32,11 @@ export const FormSchema = z.object({
|
||||
|
||||
export type TokenizerFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||
const TokenizerForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
hideOutputs,
|
||||
}: INextOperatorForm) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultValues = useFormValues(initialTokenizerValues, node);
|
||||
|
||||
@@ -53,6 +58,7 @@ const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
useFormChangeCallback(form, onValuesChange);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -81,9 +87,11 @@ const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||
{(field) => <SelectWithSearch options={FieldsOptions} {...field} />}
|
||||
</RAGFlowFormItem>
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
{!hideOutputs && (
|
||||
<div className="p-5">
|
||||
<Output list={outputList}></Output>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
15
web/src/pages/agent/hooks/use-form-change-callback.ts
Normal file
15
web/src/pages/agent/hooks/use-form-change-callback.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
|
||||
export function useFormChangeCallback(
|
||||
form: UseFormReturn<any>,
|
||||
onValuesChange?: (values: any) => void,
|
||||
) {
|
||||
const values = useWatch({ control: form.control });
|
||||
|
||||
useEffect(() => {
|
||||
if (onValuesChange) {
|
||||
onValuesChange(values);
|
||||
}
|
||||
}, [onValuesChange, values]);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export interface IOperatorForm {
|
||||
export interface INextOperatorForm {
|
||||
node?: RAGFlowNodeType;
|
||||
nodeId?: string;
|
||||
onValuesChange?(values: any): void;
|
||||
hideOutputs?: boolean;
|
||||
}
|
||||
|
||||
export interface IGenerateParameter {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ICategorizeItemResult,
|
||||
RAGFlowNodeType,
|
||||
} from '@/interfaces/database/agent';
|
||||
import { getBackendLanguage } from '@/utils/backend-runtime';
|
||||
import { buildSelectOptions } from '@/utils/component-util';
|
||||
import { buildOptions, removeUselessFieldsFromValues } from '@/utils/form';
|
||||
import { Edge, Node, XYPosition } from '@xyflow/react';
|
||||
@@ -202,10 +203,10 @@ function transformObjectArrayToPureArray(
|
||||
: [];
|
||||
}
|
||||
|
||||
function transformParserParams(params: ParserFormSchemaType) {
|
||||
export function transformParserParams(params: ParserFormSchemaType) {
|
||||
const setups = params.setups.reduce<
|
||||
Record<string, ParserFormSchemaType['setups'][0]>
|
||||
>((pre, cur) => {
|
||||
>((pre, cur, index) => {
|
||||
if (cur.fileFormat) {
|
||||
let filteredSetup: Partial<
|
||||
ParserFormSchemaType['setups'][0] & { suffix: string[] } & {
|
||||
@@ -318,15 +319,26 @@ function transformParserParams(params: ParserFormSchemaType) {
|
||||
break;
|
||||
}
|
||||
|
||||
pre[cur.fileFormat] = filteredSetup;
|
||||
pre[cur.fileFormat] = {
|
||||
...filteredSetup,
|
||||
order_index: index,
|
||||
} as any;
|
||||
}
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
// The Go backend expects the setups map flattened into top-level params,
|
||||
// while the Python backend reads them from the nested `setups` object.
|
||||
// Default to the Python shape while the language probe is unresolved.
|
||||
if (getBackendLanguage() === 'go') {
|
||||
return { ...omit(params, ['setups']), ...setups };
|
||||
}
|
||||
return { ...params, setups };
|
||||
}
|
||||
|
||||
function transformTokenChunkerParams(params: TokenChunkerFormSchemaType) {
|
||||
export function transformTokenChunkerParams(
|
||||
params: TokenChunkerFormSchemaType,
|
||||
) {
|
||||
const { image_table_context_window, ...rest } = params;
|
||||
const imageTableContextWindow = Number(image_table_context_window || 0);
|
||||
return {
|
||||
@@ -349,7 +361,9 @@ function transformTokenChunkerParams(params: TokenChunkerFormSchemaType) {
|
||||
};
|
||||
}
|
||||
|
||||
function transformTitleChunkerParams(params: TitleChunkerFormSchemaType) {
|
||||
export function transformTitleChunkerParams(
|
||||
params: TitleChunkerFormSchemaType,
|
||||
) {
|
||||
const activeRules =
|
||||
(params.method === TitleChunkerMethod.Group
|
||||
? params.groupRules
|
||||
@@ -379,7 +393,7 @@ function transformTitleChunkerParams(params: TitleChunkerFormSchemaType) {
|
||||
};
|
||||
}
|
||||
|
||||
function transformExtractorParams(params: ExtractorFormSchemaType) {
|
||||
export function transformExtractorParams(params: ExtractorFormSchemaType) {
|
||||
return { ...params, prompts: [{ content: params.prompts, role: 'user' }] };
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Spin } from '@/components/ui/spin';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchBuiltinPipelines } from '@/hooks/use-agent-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { history } from '@/utils/simple-history-util';
|
||||
import { t } from 'i18next';
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
FieldValues,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router';
|
||||
import { DataSetContext } from '..';
|
||||
import { MetadataType } from '../../components/metedata/constant';
|
||||
@@ -100,6 +102,61 @@ export function ChunkMethodItem(props: IProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function BuiltinPipelineItem({
|
||||
line = 2,
|
||||
name = 'parser_id',
|
||||
}: {
|
||||
line?: 1 | 2;
|
||||
name?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const { options: builtinPipelineOptions } = useFetchBuiltinPipelines();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div
|
||||
className={cn('flex', {
|
||||
'items-center': line === 1,
|
||||
'flex-col gap-1': line === 2,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
required
|
||||
className={cn('text-sm whitespace-wrap', {
|
||||
'w-1/4': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('knowledgeConfiguration.builtIn')}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
placeholder={t(
|
||||
'knowledgeConfiguration.chunkMethodPlaceholder',
|
||||
)}
|
||||
options={builtinPipelineOptions}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const EmbeddingSelect = ({
|
||||
isEdit,
|
||||
field,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
IDataSorceInfo,
|
||||
IDataSourceBase,
|
||||
} from '@/pages/user-setting/data-source/interface';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type IAddedSourceCardProps = IDataSorceInfo & {
|
||||
filterString: string;
|
||||
list: IDataSourceBase[];
|
||||
selectedList: IDataSourceBase[];
|
||||
setSelectedList: (list: IDataSourceBase[]) => void;
|
||||
};
|
||||
export const AddedSourceCard = (props: IAddedSourceCardProps) => {
|
||||
const {
|
||||
list: originList,
|
||||
name,
|
||||
icon,
|
||||
filterString,
|
||||
selectedList,
|
||||
setSelectedList,
|
||||
} = props;
|
||||
|
||||
const list = useMemo(() => {
|
||||
return originList.map((item) => {
|
||||
const checked = selectedList?.some((i) => i.id === item.id) || false;
|
||||
return {
|
||||
...item,
|
||||
checked: checked,
|
||||
};
|
||||
});
|
||||
}, [originList, selectedList]);
|
||||
|
||||
const filterList = useMemo(
|
||||
() => list.filter((item) => item.name.indexOf(filterString) > -1),
|
||||
[filterString, list],
|
||||
);
|
||||
|
||||
const onCheck = (item: IDataSourceBase & { checked: boolean }) => {
|
||||
if (item.checked) {
|
||||
setSelectedList(selectedList.filter((i) => i.id !== item.id));
|
||||
} else {
|
||||
setSelectedList([...(selectedList || []), item]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{filterList.length > 0 && (
|
||||
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
|
||||
<CardTitle className="text-base flex gap-1 font-normal">
|
||||
{icon}
|
||||
{name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 flex flex-col gap-2">
|
||||
{filterList.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'flex flex-row items-center justify-between rounded-md bg-bg-card px-2 py-1 cursor-pointer',
|
||||
)}
|
||||
onClick={() => {
|
||||
console.log('item--->', item);
|
||||
onCheck(item);
|
||||
}}
|
||||
>
|
||||
<div className="text-sm text-text-secondary ">{item.name}</div>
|
||||
<div className="text-sm text-text-secondary flex gap-2">
|
||||
{item.checked && (
|
||||
<Check className="cursor-pointer" size={14} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SearchInput } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { IConnector } from '@/interfaces/database/dataset';
|
||||
import { useListDataSource } from '@/pages/user-setting/data-source/hooks';
|
||||
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AddedSourceCard } from './added-source-card';
|
||||
|
||||
const LinkDataSourceModal = ({
|
||||
selectedList,
|
||||
open,
|
||||
setOpen,
|
||||
onSubmit,
|
||||
}: {
|
||||
selectedList: IConnector[];
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onSubmit?: (list: IDataSourceBase[] | undefined) => void;
|
||||
}) => {
|
||||
const [list, setList] = useState<IDataSourceBase[]>();
|
||||
const [fileterString, setFileterString] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setList(selectedList);
|
||||
}, [selectedList]);
|
||||
|
||||
const { categorizedList } = useListDataSource();
|
||||
const handleFormSubmit = (values: any) => {
|
||||
console.log(values, selectedList);
|
||||
onSubmit?.(list);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
className="!w-[560px]"
|
||||
title={t('knowledgeConfiguration.linkDataSource')}
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
setList(selectedList);
|
||||
}}
|
||||
onOpenChange={setOpen}
|
||||
showfooter={false}
|
||||
>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<SearchInput
|
||||
value={fileterString}
|
||||
onChange={(e) => setFileterString(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
{categorizedList.map((item, index) => (
|
||||
<AddedSourceCard
|
||||
key={index}
|
||||
selectedList={list as IDataSourceBase[]}
|
||||
setSelectedList={(list) => setList(list)}
|
||||
filterString={fileterString}
|
||||
{...item}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'outline'}
|
||||
className="btn-primary"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('modal.cancelText')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'default'}
|
||||
className="btn-primary"
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
{t('modal.okText')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default LinkDataSourceModal;
|
||||
213
web/src/pages/dataset/setting/components/link-data-source.tsx
Normal file
213
web/src/pages/dataset/setting/components/link-data-source.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { IconFontFill } from '@/components/icon-font';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { IConnector } from '@/interfaces/database/dataset';
|
||||
import { delSourceModal } from '@/pages/user-setting/data-source/component/delete-source-modal';
|
||||
import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
|
||||
import { useDataSourceRebuild } from '@/pages/user-setting/data-source/hooks';
|
||||
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
|
||||
import { Link, Settings, Unlink } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LinkDataSourceModal from './link-data-source-modal';
|
||||
|
||||
export type IDataSourceNodeProps = IConnector & {
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
export interface ILinkDataSourceProps {
|
||||
data?: IConnector[];
|
||||
handleLinkOrEditSubmit?: (data: IDataSourceBase[] | undefined) => void;
|
||||
unbindFunc?: (item: DataSourceItemProps) => void;
|
||||
handleAutoParse?: (option: {
|
||||
source_id: string;
|
||||
isAutoParse: boolean;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface DataSourceItemProps extends IDataSourceNodeProps {
|
||||
openLinkModalFunc?: (open: boolean, data?: IDataSourceNodeProps) => void;
|
||||
unbindFunc?: (item: DataSourceItemProps) => void;
|
||||
handleAutoParse?: (option: {
|
||||
source_id: string;
|
||||
isAutoParse: boolean;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const DataSourceItem = (props: DataSourceItemProps) => {
|
||||
const { dataSourceInfo } = useDataSourceInfo();
|
||||
const { t } = useTranslation();
|
||||
const { id, name, icon, source, auto_parse, unbindFunc, handleAutoParse } =
|
||||
props;
|
||||
|
||||
const { navigateToDataSourceDetail } = useNavigatePage();
|
||||
const { handleRebuild } = useDataSourceRebuild();
|
||||
const toDetail = (id: string) => {
|
||||
navigateToDataSourceDetail(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1 px-2 h-10 rounded-md border group hover:bg-bg-card">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-6 h-6 flex-shrink-0">{icon}</div>
|
||||
<div className="text-base text-text-primary">
|
||||
{dataSourceInfo[source].name}
|
||||
</div>
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
<div className="flex items-center ">
|
||||
<div className="items-center gap-1 hidden mr-5 group-hover:flex">
|
||||
<div className="text-xs text-text-secondary">
|
||||
{t('knowledgeConfiguration.autoParse')}
|
||||
</div>
|
||||
<Switch
|
||||
checked={auto_parse === '1'}
|
||||
onCheckedChange={(isAutoParse) => {
|
||||
handleAutoParse?.({ source_id: id, isAutoParse });
|
||||
}}
|
||||
className="w-8 h-4"
|
||||
/>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
className="border-none hidden group-hover:block"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleRebuild({ source_id: id });
|
||||
}}
|
||||
>
|
||||
<IconFontFill name="reparse" className="text-text-primary" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('knowledgeConfiguration.rebuildTip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
className="border-none hidden group-hover:block"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toDetail(id);
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'transparent'}
|
||||
className="border-none hidden group-hover:block"
|
||||
onClick={() => {
|
||||
delSourceModal({
|
||||
data: props,
|
||||
type: 'unlink',
|
||||
dataSourceInfo: dataSourceInfo,
|
||||
onOk: (data) => unbindFunc?.(data as DataSourceItemProps),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Unlink />
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkDataSource = (props: ILinkDataSourceProps) => {
|
||||
const {
|
||||
data,
|
||||
handleLinkOrEditSubmit: submit,
|
||||
unbindFunc,
|
||||
handleAutoParse,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const { dataSourceInfo } = useDataSourceInfo();
|
||||
const [openLinkModal, setOpenLinkModal] = useState(false);
|
||||
|
||||
const pipelineNode: IDataSourceNodeProps[] = useMemo(() => {
|
||||
if (data && data.length > 0) {
|
||||
return data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
id: item?.id,
|
||||
name: item?.name,
|
||||
icon:
|
||||
dataSourceInfo[item?.source as keyof typeof dataSourceInfo]?.icon ||
|
||||
'',
|
||||
} as IDataSourceNodeProps;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}, [data, dataSourceInfo]);
|
||||
|
||||
const openLinkModalFunc = (open: boolean, data?: IDataSourceNodeProps) => {
|
||||
console.log('open', open, data);
|
||||
setOpenLinkModal(open);
|
||||
};
|
||||
|
||||
const handleLinkOrEditSubmit = (data: IDataSourceBase[] | undefined) => {
|
||||
console.log('handleLinkOrEditSubmit', data);
|
||||
submit?.(data);
|
||||
setOpenLinkModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<section className="flex flex-col">
|
||||
<div className="text-base font-medium text-text-primary">
|
||||
{t('knowledgeConfiguration.dataSource')}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-center text-xs text-text-secondary">
|
||||
{t('knowledgeConfiguration.linkSourceSetTip')}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'transparent'}
|
||||
onClick={() => {
|
||||
openLinkModalFunc?.(true);
|
||||
}}
|
||||
>
|
||||
<Link />
|
||||
<span className="text-xs text-text-primary">
|
||||
{t('knowledgeConfiguration.linkDataSource')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="flex flex-col gap-2">
|
||||
{pipelineNode.map(
|
||||
(item) =>
|
||||
item.id && (
|
||||
<DataSourceItem
|
||||
key={item.id}
|
||||
openLinkModalFunc={openLinkModalFunc}
|
||||
unbindFunc={unbindFunc}
|
||||
handleAutoParse={handleAutoParse}
|
||||
{...item}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
<LinkDataSourceModal
|
||||
selectedList={data as IConnector[]}
|
||||
open={openLinkModal}
|
||||
setOpen={(open: boolean) => {
|
||||
openLinkModalFunc(open);
|
||||
}}
|
||||
onSubmit={handleLinkOrEditSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default LinkDataSource;
|
||||
130
web/src/pages/dataset/setting/embedding-model-form-field.tsx
Normal file
130
web/src/pages/dataset/setting/embedding-model-form-field.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { ModelTreeSelect, ModelTypeMap } from '@/components/model-tree-select';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FieldValues, useFormContext } from 'react-hook-form';
|
||||
import { useHandleKbEmbedding, useHasParsedDocument } from './hooks';
|
||||
|
||||
interface IProps {
|
||||
line?: 1 | 2;
|
||||
isEdit?: boolean;
|
||||
}
|
||||
|
||||
export const EmbeddingSelect = ({
|
||||
isEdit,
|
||||
field,
|
||||
name,
|
||||
disabled = false,
|
||||
testId,
|
||||
ownerTenantId,
|
||||
}: {
|
||||
isEdit: boolean;
|
||||
field: FieldValues;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
ownerTenantId?: string;
|
||||
}) => {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
const { handleChange } = useHandleKbEmbedding();
|
||||
|
||||
const oldValue = useMemo(() => {
|
||||
const embdStr = form.getValues(name || 'embedding_model');
|
||||
return embdStr || '';
|
||||
}, [form, name]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<Spin
|
||||
spinning={loading}
|
||||
className={cn('rounded-lg after:bg-bg-base', {
|
||||
'opacity-20': loading,
|
||||
})}
|
||||
>
|
||||
<ModelTreeSelect
|
||||
modelTypes={ModelTypeMap.embd_id}
|
||||
onChange={async (value) => {
|
||||
field.onChange(value);
|
||||
if (isEdit && disabled) {
|
||||
setLoading(true);
|
||||
const res = await handleChange({
|
||||
embed_id: value,
|
||||
});
|
||||
if (res.code !== 0) {
|
||||
field.onChange(oldValue);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
ownerTenantId={ownerTenantId}
|
||||
disabled={disabled && !isEdit}
|
||||
value={field.value}
|
||||
placeholder={t('embeddingModelPlaceholder')}
|
||||
testId={testId}
|
||||
/>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export function EmbeddingModelItem({
|
||||
line = 1,
|
||||
isEdit,
|
||||
ownerTenantId,
|
||||
}: IProps & { ownerTenantId?: string }) {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
const disabled = useHasParsedDocument(isEdit);
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'embedding_model'}
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn('items-center space-y-0')}>
|
||||
<div
|
||||
className={cn('flex', {
|
||||
'items-center': line === 1,
|
||||
'flex-col gap-1': line === 2,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
required
|
||||
tooltip={t('embeddingModelTip')}
|
||||
className={cn('text-sm whitespace-wrap', {
|
||||
'w-1/4': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('embeddingModel')}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
|
||||
>
|
||||
<FormControl>
|
||||
<EmbeddingSelect
|
||||
isEdit={!!isEdit}
|
||||
field={field}
|
||||
disabled={disabled}
|
||||
testId="ds-settings-basic-embedding-model-select"
|
||||
ownerTenantId={ownerTenantId}
|
||||
></EmbeddingSelect>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
web/src/pages/dataset/setting/form-schema.ts
Normal file
41
web/src/pages/dataset/setting/form-schema.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import { t } from 'i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const formSchema = z
|
||||
.object({
|
||||
parse_type: z.nativeEnum(ParseType).optional(),
|
||||
pipeline_id: z.string().optional(),
|
||||
pipeline_name: z.string().optional(),
|
||||
pipeline_avatar: z.string().optional(),
|
||||
name: z.string().min(1, {
|
||||
message: 'Username must be at least 2 characters.',
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
parser_id: z.string().optional(),
|
||||
avatar: z.any().nullish(),
|
||||
permission: z.string().optional(),
|
||||
embedding_model: z.string(),
|
||||
pagerank: z.number(),
|
||||
parser_config: z.record(z.string(), z.any()).optional(),
|
||||
connectors: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
ststus: z.string().optional(),
|
||||
auto_parse: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.parse_type === ParseType.Pipeline && !data.pipeline_id) {
|
||||
ctx.addIssue({
|
||||
path: ['pipeline_id'],
|
||||
message: t('common.pleaseSelect'),
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
});
|
||||
110
web/src/pages/dataset/setting/general-form.tsx
Normal file
110
web/src/pages/dataset/setting/general-form.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import PageRankFormField from '@/components/page-rank-form-field';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useKnowledgeBaseContext } from '../contexts/knowledge-base-context';
|
||||
import { EmbeddingModelItem } from './embedding-model-form-field';
|
||||
import { PermissionFormField } from './permission-form-field';
|
||||
|
||||
export function GeneralForm() {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('common.name')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="ds-settings-basic-name-input"
|
||||
></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatar"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
{t('setting.avatar')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<AvatarUpload
|
||||
{...field}
|
||||
uploadInputTestId="ds-settings-basic-avatar-upload"
|
||||
cropModalTestId="ds-settings-basic-avatar-crop-modal"
|
||||
cropModalOkButtonTestId="ds-settings-basic-avatar-crop-confirm-btn"
|
||||
></AvatarUpload>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
// null initialize empty string
|
||||
if (typeof field.value === 'object' && !field.value) {
|
||||
form.setValue('description', '');
|
||||
}
|
||||
return (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
{t('flow.description')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t('knowledgeConfiguration.datasetDescription')}
|
||||
data-testid="ds-settings-basic-description-input"
|
||||
></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<PermissionFormField></PermissionFormField>
|
||||
<EmbeddingModelItem
|
||||
isEdit={true}
|
||||
ownerTenantId={useKnowledgeBaseContext().knowledgeBase?.tenant_id}
|
||||
></EmbeddingModelItem>
|
||||
<PageRankFormField></PageRankFormField>
|
||||
</>
|
||||
);
|
||||
}
|
||||
280
web/src/pages/dataset/setting/hooks.ts
Normal file
280
web/src/pages/dataset/setting/hooks.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import { useFetchPipelineDslByPipelineId } from '@/hooks/use-agent-request';
|
||||
import {
|
||||
useFetchDatasetPipelineConfiguration,
|
||||
useUpdateKnowledge,
|
||||
} from '@/hooks/use-knowledge-request';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import { IConnector } from '@/interfaces/database/dataset';
|
||||
import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
|
||||
import { checkEmbedding } from '@/services/knowledge-service';
|
||||
import { pick } from 'lodash';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
import { formSchema } from './form-schema';
|
||||
import {
|
||||
buildPipelineOperatorNodes,
|
||||
getOperatorType,
|
||||
transformApiConfigToForm,
|
||||
transformFormConfigToApi,
|
||||
} from './utils';
|
||||
|
||||
export function useHasParsedDocument(isEdit?: boolean) {
|
||||
const { data: knowledgeDetails } = useFetchDatasetPipelineConfiguration({
|
||||
isEdit,
|
||||
});
|
||||
return knowledgeDetails.chunk_count > 0;
|
||||
}
|
||||
|
||||
export const useHandleKbEmbedding = () => {
|
||||
const { id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const knowledgeBaseId = searchParams.get('id') || id;
|
||||
const handleChange = useCallback(
|
||||
async ({ embed_id }: { embed_id: string }) => {
|
||||
const res = await checkEmbedding(knowledgeBaseId || '', {
|
||||
embd_id: embed_id,
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
[knowledgeBaseId],
|
||||
);
|
||||
return {
|
||||
handleChange,
|
||||
};
|
||||
};
|
||||
|
||||
export const useFetchDatasetSettingOnMount = (
|
||||
form: UseFormReturn<z.infer<typeof formSchema>>,
|
||||
) => {
|
||||
const { data: knowledgeDetails, loading } =
|
||||
useFetchDatasetPipelineConfiguration();
|
||||
const { dataSourceInfo } = useDataSourceInfo();
|
||||
|
||||
const sourceData = useMemo(() => {
|
||||
return (knowledgeDetails?.connectors ?? []).map(
|
||||
(connector: IConnector) => ({
|
||||
...connector,
|
||||
icon:
|
||||
dataSourceInfo[connector.source as keyof typeof dataSourceInfo]
|
||||
?.icon || '',
|
||||
}),
|
||||
);
|
||||
}, [knowledgeDetails?.connectors, dataSourceInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
const parserConfig = knowledgeDetails.parser_config as
|
||||
| Record<string, any>
|
||||
| undefined;
|
||||
let formParserConfig: Record<string, any> | undefined = parserConfig;
|
||||
|
||||
// Convert parser_config to form format if in pipeline mode
|
||||
if (
|
||||
parserConfig &&
|
||||
typeof parserConfig === 'object' &&
|
||||
!Array.isArray(parserConfig)
|
||||
) {
|
||||
const keys = Object.keys(parserConfig);
|
||||
const hasPipelineKeys = keys.some((key) => key.includes(':'));
|
||||
if (hasPipelineKeys) {
|
||||
formParserConfig = {};
|
||||
for (const [operatorId, config] of Object.entries(parserConfig)) {
|
||||
const operatorType = getOperatorType(operatorId);
|
||||
formParserConfig[operatorId] = transformApiConfigToForm(
|
||||
operatorType,
|
||||
config as Record<string, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formValues = {
|
||||
...pick(knowledgeDetails, [
|
||||
'description',
|
||||
'name',
|
||||
'permission',
|
||||
'connectors',
|
||||
'pagerank',
|
||||
'avatar',
|
||||
'pipeline_id',
|
||||
'pipeline_name',
|
||||
'pipeline_avatar',
|
||||
'parser_id',
|
||||
]),
|
||||
embedding_model: knowledgeDetails.embedding_model,
|
||||
connectors: sourceData,
|
||||
parse_type: knowledgeDetails.pipeline_id
|
||||
? ParseType.Pipeline
|
||||
: ParseType.BuiltIn,
|
||||
parser_config: formParserConfig,
|
||||
} as z.infer<typeof formSchema>;
|
||||
form.reset(formValues);
|
||||
}, [form, knowledgeDetails, sourceData]);
|
||||
|
||||
return { knowledgeDetails, loading, sourceData };
|
||||
};
|
||||
|
||||
export const usePipelineOperatorNodes = (
|
||||
pipelineId?: string,
|
||||
pipelineParserConfig?: Record<string, any>,
|
||||
isBuiltin = false,
|
||||
) => {
|
||||
const { dsl, loading } = useFetchPipelineDslByPipelineId(
|
||||
pipelineId,
|
||||
isBuiltin,
|
||||
);
|
||||
|
||||
const operatorNodes = useMemo(() => {
|
||||
return buildPipelineOperatorNodes(dsl, pipelineParserConfig);
|
||||
}, [dsl, pipelineParserConfig]);
|
||||
|
||||
return { operatorNodes, loading };
|
||||
};
|
||||
|
||||
export const useSaveDatasetSetting = () => {
|
||||
const { saveKnowledgeConfiguration, loading } = useUpdateKnowledge();
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (values: z.infer<typeof formSchema>) => {
|
||||
const payload = { ...values };
|
||||
|
||||
// Apply forward transforms to parser_config if in pipeline mode
|
||||
if (payload.parser_config) {
|
||||
const transformedConfig: Record<string, any> = {};
|
||||
for (const [operatorId, config] of Object.entries(
|
||||
payload.parser_config,
|
||||
)) {
|
||||
const operatorType = getOperatorType(operatorId);
|
||||
transformedConfig[operatorId] = transformFormConfigToApi(
|
||||
operatorType,
|
||||
config as Record<string, any>,
|
||||
);
|
||||
}
|
||||
payload.parser_config = transformedConfig;
|
||||
}
|
||||
|
||||
if (payload.parse_type === ParseType.BuiltIn) {
|
||||
payload.pipeline_id = undefined;
|
||||
} else {
|
||||
payload.parser_id = undefined;
|
||||
}
|
||||
return saveKnowledgeConfiguration(payload);
|
||||
},
|
||||
[saveKnowledgeConfiguration],
|
||||
);
|
||||
|
||||
return { handleSave, loading };
|
||||
};
|
||||
|
||||
export const useActiveTab = (operatorNodes: RAGFlowNodeType[]) => {
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (operatorNodes.length > 0) {
|
||||
const firstTab =
|
||||
(operatorNodes[0].data as Record<string, any>)?.operatorId ||
|
||||
operatorNodes[0].data?.label ||
|
||||
'';
|
||||
const validTabs = operatorNodes.map(
|
||||
(node) =>
|
||||
(node.data as Record<string, any>)?.operatorId ||
|
||||
node.data?.label ||
|
||||
'',
|
||||
);
|
||||
setActiveTab((prev) => (validTabs.includes(prev) ? prev : firstTab));
|
||||
} else {
|
||||
setActiveTab('');
|
||||
}
|
||||
}, [operatorNodes]);
|
||||
|
||||
return { activeTab, setActiveTab };
|
||||
};
|
||||
|
||||
export const usePipelineDataList = (sourceData: any[] | undefined) => {
|
||||
const { dataSourceInfo } = useDataSourceInfo();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
sourceData?.map((item) => ({
|
||||
...item,
|
||||
icon:
|
||||
dataSourceInfo[item.source as keyof typeof dataSourceInfo]?.icon ||
|
||||
'',
|
||||
})),
|
||||
[sourceData, dataSourceInfo],
|
||||
);
|
||||
};
|
||||
|
||||
export const useConnectorHandlers = (
|
||||
form: UseFormReturn<any>,
|
||||
sourceData?: any[],
|
||||
setSourceData?: Dispatch<SetStateAction<any[] | undefined>>,
|
||||
) => {
|
||||
const { dataSourceInfo } = useDataSourceInfo();
|
||||
|
||||
const handleLinkOrEditSubmit = useCallback(
|
||||
(data: any[] | undefined) => {
|
||||
if (data) {
|
||||
const connectors = data.map((connector) => ({
|
||||
...connector,
|
||||
auto_parse: (connector as IConnector).auto_parse === '0' ? '0' : '1',
|
||||
icon:
|
||||
dataSourceInfo[connector.source as keyof typeof dataSourceInfo]
|
||||
?.icon || '',
|
||||
}));
|
||||
setSourceData?.(connectors);
|
||||
form.setValue('connectors', connectors || []);
|
||||
}
|
||||
},
|
||||
[dataSourceInfo, form, setSourceData],
|
||||
);
|
||||
|
||||
const unbindFunc = useCallback(
|
||||
(data: any) => {
|
||||
if (data) {
|
||||
const connectors = sourceData?.filter(
|
||||
(connector) => connector.id !== data.id,
|
||||
);
|
||||
setSourceData?.(connectors);
|
||||
form.setValue('connectors', connectors || []);
|
||||
}
|
||||
},
|
||||
[sourceData, form, setSourceData],
|
||||
);
|
||||
|
||||
const handleAutoParse = useCallback(
|
||||
({
|
||||
source_id,
|
||||
isAutoParse,
|
||||
}: {
|
||||
source_id: string;
|
||||
isAutoParse: boolean;
|
||||
}) => {
|
||||
if (source_id) {
|
||||
const connectors = sourceData?.map((connector) => {
|
||||
if (connector.id === source_id) {
|
||||
return {
|
||||
...connector,
|
||||
auto_parse: isAutoParse ? '1' : '0',
|
||||
};
|
||||
}
|
||||
return connector;
|
||||
});
|
||||
setSourceData?.(connectors);
|
||||
form.setValue('connectors', connectors || []);
|
||||
}
|
||||
},
|
||||
[sourceData, form, setSourceData],
|
||||
);
|
||||
|
||||
return { handleLinkOrEditSubmit, unbindFunc, handleAutoParse };
|
||||
};
|
||||
225
web/src/pages/dataset/setting/index.tsx
Normal file
225
web/src/pages/dataset/setting/index.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { DataFlowSelect } from '@/components/data-pipeline-select';
|
||||
import { Button, ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import Divider from '@/components/ui/divider';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import {
|
||||
BuiltinPipelineItem,
|
||||
ParseTypeItem,
|
||||
} from '@/pages/dataset/dataset-setting/configuration/common-item';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import LinkDataSource, {
|
||||
IDataSourceNodeProps,
|
||||
} from './components/link-data-source';
|
||||
import { formSchema } from './form-schema';
|
||||
import { GeneralForm } from './general-form';
|
||||
import {
|
||||
useActiveTab,
|
||||
useConnectorHandlers,
|
||||
useFetchDatasetSettingOnMount,
|
||||
usePipelineDataList,
|
||||
usePipelineOperatorNodes,
|
||||
useSaveDatasetSetting,
|
||||
} from './hooks';
|
||||
import PipelineOperatorTabs from './pipeline-operator-tabs';
|
||||
|
||||
export default function DatasetSetting() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
parse_type: ParseType.BuiltIn,
|
||||
pipeline_id: '',
|
||||
pipeline_name: '',
|
||||
pipeline_avatar: '',
|
||||
parser_id: '',
|
||||
parser_config: {},
|
||||
name: '',
|
||||
description: '',
|
||||
avatar: null,
|
||||
permission: '',
|
||||
embedding_model: '',
|
||||
pagerank: 0,
|
||||
connectors: [],
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
knowledgeDetails,
|
||||
loading: datasetSettingLoading,
|
||||
sourceData,
|
||||
} = useFetchDatasetSettingOnMount(form);
|
||||
const { handleSave, loading: saveLoading } = useSaveDatasetSetting();
|
||||
|
||||
const [sourceDataState, setSourceDataState] =
|
||||
useState<IDataSourceNodeProps[]>();
|
||||
|
||||
useEffect(() => {
|
||||
setSourceDataState(sourceData);
|
||||
}, [sourceData]);
|
||||
|
||||
const parseType = useWatch({
|
||||
control: form.control,
|
||||
name: 'parse_type',
|
||||
defaultValue: ParseType.BuiltIn,
|
||||
});
|
||||
const pipelineId = useWatch({
|
||||
control: form.control,
|
||||
name: 'pipeline_id',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const builtinPipelineId = useWatch({
|
||||
control: form.control,
|
||||
name: 'parser_id',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const pipelineParserConfig = knowledgeDetails?.parser_config as
|
||||
| Record<string, any>
|
||||
| undefined;
|
||||
|
||||
const { operatorNodes } = usePipelineOperatorNodes(
|
||||
parseType === ParseType.Pipeline ? pipelineId : builtinPipelineId,
|
||||
pipelineParserConfig,
|
||||
parseType === ParseType.BuiltIn,
|
||||
);
|
||||
|
||||
const { activeTab, setActiveTab } = useActiveTab(operatorNodes);
|
||||
|
||||
useEffect(() => {
|
||||
if (parseType === ParseType.BuiltIn) {
|
||||
form.setValue('pipeline_id', '');
|
||||
form.setValue('pipeline_name', '');
|
||||
form.setValue('pipeline_avatar', '');
|
||||
}
|
||||
}, [parseType, form]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (data: z.infer<typeof formSchema>) => {
|
||||
await handleSave(data);
|
||||
},
|
||||
[handleSave],
|
||||
);
|
||||
|
||||
const { handleLinkOrEditSubmit, unbindFunc, handleAutoParse } =
|
||||
useConnectorHandlers(form, sourceDataState, setSourceDataState);
|
||||
|
||||
const handleOperatorValuesChange = useCallback(
|
||||
(operatorId: string, values: any) => {
|
||||
const currentParserConfig = form.getValues('parser_config') || {};
|
||||
form.setValue('parser_config', {
|
||||
...currentParserConfig,
|
||||
[operatorId]: values,
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const pipelineDataList = usePipelineDataList(sourceDataState);
|
||||
|
||||
const showOperatorTabs =
|
||||
operatorNodes.length > 0 &&
|
||||
((parseType === ParseType.Pipeline && !!pipelineId) ||
|
||||
(parseType === ParseType.BuiltIn && !!builtinPipelineId));
|
||||
|
||||
return (
|
||||
<div className="pr-5 pb-5">
|
||||
<Card className="p-0 h-full flex flex-col bg-transparent shadow-none">
|
||||
<CardHeader className="p-5 border-b-0.5 border-border-button">
|
||||
<header>
|
||||
<CardTitle as="h1">
|
||||
{t('knowledgeDetails.nextConfiguration')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('knowledgeConfiguration.titleDescription')}
|
||||
</CardDescription>
|
||||
</header>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0 flex-1 h-0 flex">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<div className="flex-1 h-0 w-[768px] px-5 pt-5 overflow-y-auto scrollbar-auto">
|
||||
<section className="space-y-5 text-text-secondary">
|
||||
<div className="text-base font-medium text-text-primary">
|
||||
{t('knowledgeConfiguration.baseInfo')}
|
||||
</div>
|
||||
<GeneralForm></GeneralForm>
|
||||
|
||||
<Divider />
|
||||
<div className="text-base font-medium text-text-primary">
|
||||
{t('knowledgeConfiguration.dataPipeline')}
|
||||
</div>
|
||||
<ParseTypeItem line={1} name="parse_type" />
|
||||
{parseType === ParseType.BuiltIn && (
|
||||
<BuiltinPipelineItem line={1} name="parser_id" />
|
||||
)}
|
||||
{parseType === ParseType.Pipeline && (
|
||||
<DataFlowSelect
|
||||
isMult={false}
|
||||
showToDataPipeline={true}
|
||||
formFieldName="pipeline_id"
|
||||
layout={FormLayout.Horizontal}
|
||||
/>
|
||||
)}
|
||||
{showOperatorTabs && (
|
||||
<PipelineOperatorTabs
|
||||
nodes={operatorNodes}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
onOperatorValuesChange={handleOperatorValuesChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<LinkDataSource
|
||||
data={pipelineDataList}
|
||||
handleLinkOrEditSubmit={handleLinkOrEditSubmit}
|
||||
unbindFunc={unbindFunc}
|
||||
handleAutoParse={handleAutoParse}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="p-5 text-right items-center flex justify-end gap-3 w-[768px]">
|
||||
<Button
|
||||
type="reset"
|
||||
variant="transparent"
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
{t('knowledgeConfiguration.cancel')}
|
||||
</Button>
|
||||
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
loading={datasetSettingLoading || saveLoading}
|
||||
>
|
||||
{t('knowledgeConfiguration.save')}
|
||||
</ButtonLoading>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
web/src/pages/dataset/setting/permission-form-field.tsx
Normal file
30
web/src/pages/dataset/setting/permission-form-field.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { PermissionRole } from '@/constants/permission';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function PermissionFormField() {
|
||||
const { t } = useTranslation();
|
||||
const teamOptions = useMemo(() => {
|
||||
return Object.values(PermissionRole).map((x) => ({
|
||||
label: t('knowledgeConfiguration.' + x),
|
||||
value: x,
|
||||
}));
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name="permission"
|
||||
label={t('knowledgeConfiguration.permissions')}
|
||||
tooltip={t('knowledgeConfiguration.permissionsTip')}
|
||||
horizontal
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={teamOptions}
|
||||
triggerClassName="w-full"
|
||||
testId="ds-settings-basic-permissions-select"
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
77
web/src/pages/dataset/setting/pipeline-operator-form.tsx
Normal file
77
web/src/pages/dataset/setting/pipeline-operator-form.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Operator } from '@/constants/agent';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import { memo, useCallback } from 'react';
|
||||
import ExtractorForm from '../../agent/form/extractor-form';
|
||||
import ParserForm from '../../agent/form/parser-form';
|
||||
import TitleChunkerForm from '../../agent/form/title-chunker-form';
|
||||
import TokenChunkerForm from '../../agent/form/token-chunker-form';
|
||||
import TokenizerForm from '../../agent/form/tokenizer-form';
|
||||
import { getOperatorType } from './utils';
|
||||
|
||||
type PipelineOperatorFormProps = {
|
||||
node: RAGFlowNodeType;
|
||||
onValuesChange?: (values: any) => void;
|
||||
};
|
||||
|
||||
const PipelineOperatorForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
}: PipelineOperatorFormProps) => {
|
||||
const operatorType = getOperatorType(
|
||||
(node.data as Record<string, any>)?.operatorId || node.data?.label || '',
|
||||
);
|
||||
|
||||
const handleValuesChange = useCallback(
|
||||
(values: any) => {
|
||||
onValuesChange?.(values);
|
||||
},
|
||||
[onValuesChange],
|
||||
);
|
||||
|
||||
switch (operatorType) {
|
||||
case Operator.Parser:
|
||||
return (
|
||||
<ParserForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.TokenChunker:
|
||||
return (
|
||||
<TokenChunkerForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.TitleChunker:
|
||||
return (
|
||||
<TitleChunkerForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.Extractor:
|
||||
return (
|
||||
<ExtractorForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.Tokenizer:
|
||||
return (
|
||||
<TokenizerForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(PipelineOperatorForm);
|
||||
66
web/src/pages/dataset/setting/pipeline-operator-tabs.tsx
Normal file
66
web/src/pages/dataset/setting/pipeline-operator-tabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import { memo, useCallback } from 'react';
|
||||
import PipelineOperatorForm from './pipeline-operator-form';
|
||||
|
||||
type PipelineOperatorTabsProps = {
|
||||
nodes: RAGFlowNodeType[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
onOperatorValuesChange: (operatorId: string, values: any) => void;
|
||||
};
|
||||
|
||||
const PipelineOperatorTabs = ({
|
||||
nodes,
|
||||
value,
|
||||
onValueChange,
|
||||
onOperatorValuesChange,
|
||||
}: PipelineOperatorTabsProps) => {
|
||||
const getOperatorId = useCallback((node: RAGFlowNodeType) => {
|
||||
return (
|
||||
(node.data as Record<string, any>)?.operatorId || node.data?.label || ''
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getTabValue = useCallback(
|
||||
(node: RAGFlowNodeType, index: number) => {
|
||||
return getOperatorId(node) || String(index);
|
||||
},
|
||||
[getOperatorId],
|
||||
);
|
||||
|
||||
const handleValuesChange = useCallback(
|
||||
(node: RAGFlowNodeType) => (values: any) => {
|
||||
onOperatorValuesChange(getOperatorId(node), values);
|
||||
},
|
||||
[getOperatorId, onOperatorValuesChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs value={value} onValueChange={onValueChange} className="w-full">
|
||||
<TabsList className="w-full justify-start">
|
||||
{nodes.map((node, index) => {
|
||||
const tabValue = getTabValue(node, index);
|
||||
return (
|
||||
<TabsTrigger key={tabValue} value={tabValue}>
|
||||
{node.data?.name || node.data?.label || tabValue}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
{nodes.map((node, index) => {
|
||||
const tabValue = getTabValue(node, index);
|
||||
return (
|
||||
<TabsContent key={tabValue} value={tabValue}>
|
||||
<PipelineOperatorForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange(node)}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PipelineOperatorTabs);
|
||||
345
web/src/pages/dataset/setting/utils.ts
Normal file
345
web/src/pages/dataset/setting/utils.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { Operator } from '@/constants/agent';
|
||||
import { DSL, RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import {
|
||||
initialExtractorValues,
|
||||
initialParserValues,
|
||||
initialTitleChunkerValues,
|
||||
initialTokenChunkerValues,
|
||||
initialTokenizerValues,
|
||||
} from '@/pages/agent/constant/pipeline';
|
||||
import {
|
||||
transformExtractorParams,
|
||||
transformParserParams,
|
||||
transformTitleChunkerParams,
|
||||
transformTokenChunkerParams,
|
||||
} from '@/pages/agent/utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export const FileNodeId = 'File';
|
||||
|
||||
export function getOperatorType(operatorId: string): Operator {
|
||||
return (operatorId.split(':')[0] || operatorId) as Operator;
|
||||
}
|
||||
|
||||
export function transformParserConfigSetups(
|
||||
setups: Record<string, any> | undefined,
|
||||
): any[] {
|
||||
if (!setups || typeof setups !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(setups)
|
||||
.map(([fileFormat, config]) => ({
|
||||
fileFormat,
|
||||
...config,
|
||||
}))
|
||||
.sort((a, b) => (a.order_index ?? Infinity) - (b.order_index ?? Infinity));
|
||||
}
|
||||
|
||||
function transformLevelsToRules(
|
||||
levels: any[],
|
||||
): Array<{ levels: Array<{ expression: string }> }> {
|
||||
if (!Array.isArray(levels)) return [];
|
||||
return levels
|
||||
.map((levelGroup) => {
|
||||
if (Array.isArray(levelGroup)) {
|
||||
const filteredExpressions = levelGroup.filter(
|
||||
(expr: string) => expr && expr.trim() !== '',
|
||||
);
|
||||
if (filteredExpressions.length === 0) return null;
|
||||
return {
|
||||
levels: filteredExpressions.map((expression: string) => ({
|
||||
expression,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { levels: [{ expression: '' }] };
|
||||
})
|
||||
.filter((rule) => rule !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Extractor config from API/DSL format to form format.
|
||||
* DSL: { prompts: [{ content: "text", role: "user" }] }
|
||||
* Form: { prompts: "text" }
|
||||
*/
|
||||
function transformExtractorConfigToForm(
|
||||
config: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
if (!config) return {};
|
||||
|
||||
const result = { ...config };
|
||||
if (Array.isArray(config.prompts) && config.prompts.length > 0) {
|
||||
result.prompts = config.prompts[0]?.content ?? '';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts TokenChunker config from API/DSL format to form format.
|
||||
* DSL: { delimiters: ["\n"], overlapped_percent: 0.1, table_context_size: 10, image_context_size: 20 }
|
||||
* Form: { delimiters: [{ value: "\n" }], overlapped_percent: 10, image_table_context_window: 15, enable_children: false }
|
||||
*/
|
||||
function transformTokenChunkerConfigToForm(
|
||||
config: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
if (!config) return {};
|
||||
|
||||
const result = { ...config };
|
||||
|
||||
// Convert string array delimiters to object array
|
||||
if (Array.isArray(config.delimiters)) {
|
||||
result.delimiters = config.delimiters.map((d: string) => ({ value: d }));
|
||||
}
|
||||
if (Array.isArray(config.children_delimiters)) {
|
||||
result.children_delimiters = config.children_delimiters.map(
|
||||
(d: string) => ({ value: d }),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert overlapped_percent from 0-1 scale to 0-100 scale
|
||||
if (typeof config.overlapped_percent === 'number') {
|
||||
result.overlapped_percent = Math.round(config.overlapped_percent * 100);
|
||||
}
|
||||
|
||||
// Merge table_context_size and image_context_size into image_table_context_window
|
||||
const tableSize = Number(config.table_context_size ?? 0);
|
||||
const imageSize = Number(config.image_context_size ?? 0);
|
||||
result.image_table_context_window = Math.max(tableSize, imageSize);
|
||||
|
||||
// Derive delimiter_mode from data
|
||||
if (config.delimiter_mode === undefined) {
|
||||
const hasDelimiters =
|
||||
Array.isArray(config.delimiters) && config.delimiters.length > 0;
|
||||
result.delimiter_mode = hasDelimiters ? 'delimiter' : 'token_size';
|
||||
}
|
||||
|
||||
// Derive enable_children from presence of children_delimiters
|
||||
if (config.enable_children === undefined) {
|
||||
result.enable_children =
|
||||
Array.isArray(config.children_delimiters) &&
|
||||
config.children_delimiters.length > 0;
|
||||
}
|
||||
|
||||
// Clean up DSL-only fields not in form schema
|
||||
delete result.table_context_size;
|
||||
delete result.image_context_size;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts TitleChunker config from API/DSL format to form format.
|
||||
* DSL: { method: "hierarchy", hierarchy: "3", levels: [...], include_heading_content, root_chunk_as_heading }
|
||||
* Form: { method, hierarchyHierarchy, hierarchyGroup, include_heading_content, root_chunk_as_heading, hierarchyRules, groupRules }
|
||||
*/
|
||||
function transformTitleChunkerConfigToForm(
|
||||
config: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
if (!config) return {};
|
||||
|
||||
const result = { ...config };
|
||||
const method = config.method ?? 'hierarchy';
|
||||
|
||||
// Convert legacy `hierarchy` (single field) to split fields
|
||||
let hierarchy = config.hierarchy;
|
||||
if (typeof hierarchy === 'number') {
|
||||
hierarchy = String(hierarchy);
|
||||
}
|
||||
|
||||
if (method === 'hierarchy') {
|
||||
result.hierarchyHierarchy = config.hierarchyHierarchy || hierarchy || '3';
|
||||
result.hierarchyGroup = config.hierarchyGroup || '0';
|
||||
} else {
|
||||
result.hierarchyHierarchy = config.hierarchyHierarchy || hierarchy || '3';
|
||||
result.hierarchyGroup = config.hierarchyGroup || hierarchy || '0';
|
||||
}
|
||||
|
||||
// Convert `levels` to `hierarchyRules`/`groupRules`
|
||||
if (config.levels && Array.isArray(config.levels) && !config.hierarchyRules) {
|
||||
result.hierarchyRules = transformLevelsToRules(config.levels);
|
||||
}
|
||||
if (config.levels && Array.isArray(config.levels) && !config.groupRules) {
|
||||
result.groupRules = transformLevelsToRules(config.levels);
|
||||
}
|
||||
|
||||
// Clean up DSL-only fields
|
||||
delete result.hierarchy;
|
||||
delete result.levels;
|
||||
delete result.rules;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizer is a passthrough — both API and form formats are identical.
|
||||
*/
|
||||
function transformTokenizerConfigToForm(
|
||||
config: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
return config ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches reverse transform by operator type.
|
||||
* Converts DSL-format config to form-format config.
|
||||
*/
|
||||
export function transformApiConfigToForm(
|
||||
operatorType: string,
|
||||
config: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
switch (operatorType) {
|
||||
case Operator.Parser:
|
||||
return { setups: transformParserConfigSetups(config) };
|
||||
case Operator.Extractor:
|
||||
return transformExtractorConfigToForm(config);
|
||||
case Operator.Tokenizer:
|
||||
return transformTokenizerConfigToForm(config);
|
||||
case Operator.TokenChunker:
|
||||
return transformTokenChunkerConfigToForm(config);
|
||||
case Operator.TitleChunker:
|
||||
return transformTitleChunkerConfigToForm(config);
|
||||
default:
|
||||
return config ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches forward transform by operator type.
|
||||
* Converts form-format config to DSL format for saving.
|
||||
*/
|
||||
export function transformFormConfigToApi(
|
||||
operatorType: string,
|
||||
config: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
if (!config) return {};
|
||||
|
||||
switch (operatorType) {
|
||||
case Operator.Parser:
|
||||
return transformParserParams(config as any);
|
||||
case Operator.Extractor:
|
||||
return transformExtractorParams(config as any);
|
||||
case Operator.Tokenizer:
|
||||
return config; // passthrough for Tokenizer
|
||||
case Operator.TokenChunker:
|
||||
return transformTokenChunkerParams(config as any);
|
||||
case Operator.TitleChunker:
|
||||
return transformTitleChunkerParams(config as any);
|
||||
default:
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOperatorForm(
|
||||
operatorId: string,
|
||||
rawForm: Record<string, any> | undefined,
|
||||
): Record<string, any> {
|
||||
const operatorType = getOperatorType(operatorId);
|
||||
|
||||
switch (operatorType) {
|
||||
case Operator.Parser: {
|
||||
return {
|
||||
...cloneDeep(initialParserValues),
|
||||
...rawForm,
|
||||
setups: rawForm?.setups?.length
|
||||
? rawForm.setups
|
||||
: cloneDeep(initialParserValues.setups),
|
||||
};
|
||||
}
|
||||
case Operator.TitleChunker:
|
||||
return {
|
||||
...cloneDeep(initialTitleChunkerValues),
|
||||
...rawForm,
|
||||
};
|
||||
case Operator.TokenChunker:
|
||||
return {
|
||||
...cloneDeep(initialTokenChunkerValues),
|
||||
...rawForm,
|
||||
};
|
||||
case Operator.Extractor:
|
||||
return {
|
||||
...cloneDeep(initialExtractorValues),
|
||||
...rawForm,
|
||||
};
|
||||
case Operator.Tokenizer:
|
||||
return {
|
||||
...cloneDeep(initialTokenizerValues),
|
||||
...rawForm,
|
||||
};
|
||||
default:
|
||||
return rawForm ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildOperatorNode(
|
||||
dslNode: RAGFlowNodeType,
|
||||
pipelineParserConfig: Record<string, any> = {},
|
||||
): RAGFlowNodeType {
|
||||
const operatorId = dslNode.id;
|
||||
const operatorType = getOperatorType(operatorId);
|
||||
|
||||
// DSL-format config from API's parser_config
|
||||
const configFromApi = operatorId
|
||||
? pipelineParserConfig[operatorId]
|
||||
: undefined;
|
||||
|
||||
// Form-format config from DSL graph
|
||||
const configFromDsl = dslNode.data?.form;
|
||||
|
||||
// Convert API config to form format, then merge (DSL template is baseline, API overrides)
|
||||
const convertedApiConfig = transformApiConfigToForm(
|
||||
operatorType,
|
||||
configFromApi,
|
||||
);
|
||||
const rawForm = {
|
||||
...configFromDsl, // template defaults from DSL (form format)
|
||||
...convertedApiConfig, // user overrides from API (now also form format)
|
||||
};
|
||||
|
||||
return {
|
||||
...dslNode,
|
||||
id: '',
|
||||
data: {
|
||||
...dslNode.data,
|
||||
label: operatorType,
|
||||
operatorId,
|
||||
form: normalizeOperatorForm(operatorId, rawForm),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPipelineOperatorNodes(
|
||||
dsl?: DSL,
|
||||
pipelineParserConfig: Record<string, any> = {},
|
||||
): RAGFlowNodeType[] {
|
||||
if (!dsl?.graph?.nodes || !dsl?.graph?.edges) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build source → target map from edges (pipeline is linear)
|
||||
const sourceToTarget = new Map<string, string>();
|
||||
for (const edge of dsl.graph.edges) {
|
||||
sourceToTarget.set(edge.source, edge.target);
|
||||
}
|
||||
|
||||
// Follow the chain starting from File, collecting node IDs in order
|
||||
const orderedIds: string[] = [];
|
||||
let currentId: string | undefined = FileNodeId;
|
||||
while (currentId) {
|
||||
orderedIds.push(currentId);
|
||||
currentId = sourceToTarget.get(currentId);
|
||||
}
|
||||
|
||||
// Build a lookup from node ID → node
|
||||
const nodeById = new Map<string, RAGFlowNodeType>();
|
||||
for (const node of dsl.graph.nodes) {
|
||||
nodeById.set(node.id, node);
|
||||
}
|
||||
|
||||
// Map ordered IDs to nodes, excluding File
|
||||
return orderedIds
|
||||
.filter((id) => id !== FileNodeId)
|
||||
.map((id) => nodeById.get(id))
|
||||
.filter((node): node is RAGFlowNodeType => node !== undefined)
|
||||
.map((node) => buildOperatorNode(node, pipelineParserConfig));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
LucideBookText,
|
||||
LucideCog,
|
||||
LucideFolderOpen,
|
||||
LucideLogs,
|
||||
LucideSettings,
|
||||
@@ -18,6 +19,7 @@ import { formatPureDate } from '@/utils/date';
|
||||
|
||||
import { IDataset } from '@/interfaces/database/dataset';
|
||||
import { useParams } from 'react-router';
|
||||
import { getBackendLanguage } from '@/utils/backend-runtime';
|
||||
|
||||
type PropType = {
|
||||
refreshCount?: number;
|
||||
@@ -46,11 +48,24 @@ export function SideBar({ dataset: data }: PropType) {
|
||||
label: t(`knowledgeDetails.overview`),
|
||||
key: Routes.DataSetOverview,
|
||||
},
|
||||
{
|
||||
icon: <LucideSettings className="size-[1em]" />,
|
||||
label: t(`knowledgeDetails.configuration`),
|
||||
key: Routes.DataSetSetting,
|
||||
},
|
||||
...(getBackendLanguage() === 'python'
|
||||
? [
|
||||
{
|
||||
icon: <LucideSettings className="size-[1em]" />,
|
||||
label: t(`knowledgeDetails.configuration`),
|
||||
key: Routes.DataSetSetting,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(getBackendLanguage() === 'go'
|
||||
? [
|
||||
{
|
||||
icon: <LucideCog className="size-[1em]" />,
|
||||
label: t(`knowledgeDetails.configuration`),
|
||||
key: Routes.DataSetSettingNext,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: <LucideBookText className="size-[1em]" />,
|
||||
label: t(`knowledgeDetails.compilation`),
|
||||
|
||||
@@ -28,14 +28,14 @@ import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ChunkMethodItem,
|
||||
BuiltinPipelineItem,
|
||||
EmbeddingModelItem,
|
||||
ParseTypeItem,
|
||||
} from '../dataset/dataset-setting/configuration/common-item';
|
||||
|
||||
const FormId = 'dataset-creating-form';
|
||||
|
||||
const ChunkMethodName = 'chunk_method';
|
||||
const ChunkMethodName = 'parser_id';
|
||||
|
||||
export function InputForm({ onOk }: IModalProps<any>) {
|
||||
const { t } = useTranslation();
|
||||
@@ -98,7 +98,9 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
const nextData =
|
||||
parseType === ParseType.BuiltIn ? data : omit(data, ChunkMethodName);
|
||||
parseType === ParseType.BuiltIn
|
||||
? omit(data, ['pipeline_id'])
|
||||
: omit(data, [ChunkMethodName]);
|
||||
onOk?.(nextData);
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ export function InputForm({ onOk }: IModalProps<any>) {
|
||||
<EmbeddingModelItem line={2} isEdit={false} />
|
||||
<ParseTypeItem />
|
||||
{parseType === ParseType.BuiltIn && (
|
||||
<ChunkMethodItem name={ChunkMethodName}></ChunkMethodItem>
|
||||
<BuiltinPipelineItem name={ChunkMethodName} />
|
||||
)}
|
||||
{parseType === ParseType.Pipeline && (
|
||||
<DataFlowSelect
|
||||
|
||||
@@ -70,6 +70,7 @@ export enum Routes {
|
||||
UserSetting = '/user-setting',
|
||||
DataSetOverview = '/logs',
|
||||
DataSetSetting = '/configuration',
|
||||
DataSetSettingNext = '/setting',
|
||||
DataflowResult = '/dataflow-result',
|
||||
Admin = '/admin',
|
||||
AdminServices = `${Admin}/services`,
|
||||
@@ -206,6 +207,10 @@ const routeConfigOptions = [
|
||||
path: `${Routes.DatasetBase}${Routes.DataSetSetting}/:id`,
|
||||
Component: () => import('@/pages/dataset/dataset-setting'),
|
||||
},
|
||||
{
|
||||
path: `${Routes.DatasetBase}${Routes.DataSetSettingNext}/:id`,
|
||||
Component: () => import('@/pages/dataset/setting'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,6 +27,8 @@ const {
|
||||
prompt,
|
||||
cancelDataflow,
|
||||
cancelCanvas,
|
||||
listBuiltinPipelines,
|
||||
getBuiltinPipeline,
|
||||
} = api;
|
||||
|
||||
const methods = {
|
||||
@@ -122,6 +124,14 @@ const methods = {
|
||||
url: api.createAgentSession,
|
||||
method: 'post',
|
||||
},
|
||||
listBuiltinPipelines: {
|
||||
url: listBuiltinPipelines,
|
||||
method: 'get',
|
||||
},
|
||||
getBuiltinPipeline: {
|
||||
url: getBuiltinPipeline,
|
||||
method: 'get',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const agentService = registerNextServer<keyof typeof methods>(methods);
|
||||
|
||||
@@ -408,6 +408,8 @@ export default {
|
||||
removeDataflow: `${webAPI}/dataflow/rm`,
|
||||
listDataflow: `${webAPI}/dataflow/list`,
|
||||
runDataflow: `${webAPI}/dataflow/run`,
|
||||
listBuiltinPipelines: `${restAPIv1}/pipelines`,
|
||||
getBuiltinPipeline: (id: string) => `${restAPIv1}/pipelines/${id}`,
|
||||
|
||||
// admin
|
||||
adminLogin: `${restAPIv1}/admin/login`,
|
||||
|
||||
Reference in New Issue
Block a user