Feat: Configure the relevant pipeline parameters on the dataset configuration page. (#17100)

This commit is contained in:
balibabu
2026-07-20 16:05:03 +08:00
committed by GitHub
parent e87008e26c
commit 9d850e782b
34 changed files with 2078 additions and 178 deletions

View File

@@ -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>

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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,

View File

@@ -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;

View File

@@ -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',

View File

@@ -445,6 +445,7 @@ export default {
dataset: '知识库',
testing: '检索测试',
configuration: '配置',
nextConfiguration: '下个配置',
knowledgeGraph: '知识图谱',
compilation: '编译',
export: '导出',

View File

@@ -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: [] },
},

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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]);
}

View File

@@ -11,6 +11,8 @@ export interface IOperatorForm {
export interface INextOperatorForm {
node?: RAGFlowNodeType;
nodeId?: string;
onValuesChange?(values: any): void;
hideOutputs?: boolean;
}
export interface IGenerateParameter {

View File

@@ -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' }] };
}

View File

@@ -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,

View File

@@ -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>
)}
</>
);
};

View File

@@ -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;

View 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;

View 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>
)}
/>
</>
);
}

View 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',
});
}
});

View 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>
</>
);
}

View 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 };
};

View 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>
);
}

View 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>
);
}

View 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);

View 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);

View 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));
}

View File

@@ -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`),

View File

@@ -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

View File

@@ -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'),
},
],
},
{

View File

@@ -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);

View File

@@ -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`,