Feat: Added support for session graph and session essence templates. (#16851)

### Summary

Feat: Added support for session graph and session essence templates.
This commit is contained in:
balibabu
2026-07-13 14:48:02 +08:00
committed by GitHub
parent d549194562
commit 2b9569ff51
40 changed files with 212 additions and 827 deletions

View File

@@ -59,6 +59,16 @@ function ArtifactForceGraph<TNodeValue = IArtifactGraphEntity>({
[onNodeClick, mapNodeToValue],
);
const getLinkColor = useCallback(() => {
if (typeof window === 'undefined' || !containerRef.current) {
return '#b2b5b7';
}
return window
.getComputedStyle(containerRef.current)
.getPropertyValue('--text-disabled')
.trim();
}, []);
return (
<div
ref={containerRef}
@@ -77,6 +87,7 @@ function ArtifactForceGraph<TNodeValue = IArtifactGraphEntity>({
onNodeClick={handleNodeClick}
nodeCanvasObject={renderNodeLabel}
nodeCanvasObjectMode={() => 'after'}
linkColor={getLinkColor}
/>
)}
</div>

View File

@@ -20,7 +20,7 @@ export interface IChunkListResult {
searchString?: string;
handleInputChange?: React.ChangeEventHandler<HTMLInputElement>;
pagination: PaginationProps;
setPagination?: (pagination: { page: number; pageSize: number }) => void;
setPagination?: (pagination: { page: number; pageSize?: number }) => void;
available: number | undefined;
handleSetAvailable: (available: number | undefined) => void;
dataUpdatedAt?: number; // Timestamp when data was last updated - useful for cache busting

View File

@@ -12,9 +12,11 @@ import {
getCompilationTemplateGroup,
updateCompilationTemplateGroup,
} from '@/services/compilation-template-group-service';
import { isCreateCompilationTemplateGroup } from '@/utils/compilation-template-util';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useCallback, useMemo } from 'react';
import { useParams } from 'react-router';
import {
useGetPaginationWithRouter,
@@ -98,15 +100,18 @@ export const useFetchCompilationTemplateGroupsByPage = () => {
};
};
export const useFetchCompilationTemplateGroup = (id?: string) => {
export const useFetchCompilationTemplateGroup = () => {
const { id } = useParams<{ id: string }>();
const isCreate = isCreateCompilationTemplateGroup(id);
const { data, isFetching: loading } = useQuery<
ICompilationTemplateGroup | undefined
>({
queryKey: CompilationTemplateGroupKeys.detail(id),
enabled: !!id && id !== 'create',
enabled: !isCreate,
gcTime: 0,
queryFn: async () => {
if (!id || id === 'create') return undefined;
if (isCreate) return undefined;
const { data } = await getCompilationTemplateGroup(id);
return data?.data as ICompilationTemplateGroup | undefined;
},

View File

@@ -27,6 +27,7 @@ export interface ICompilationTemplateConfig {
[section: string]:
| ICompilationTemplateSection
| ICompilationTemplateRaptorConfig
| Record<string, unknown>
| string
| boolean
| undefined;

View File

@@ -27,6 +27,7 @@ export interface ICompilationTemplateConfigRequest {
[section: string]:
| ICompilationTemplateSectionRequest
| ICompilationTemplateRaptorConfigRequest
| Record<string, unknown>
| string
| boolean
| undefined;

View File

@@ -103,6 +103,7 @@ export default function ToolbarPlugin({ onToggleSource, showSource }: Props) {
Source
</button>
<button
type="button"
onClick={() => {
setShowToc(!showToc);
}}

View File

@@ -517,7 +517,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
graphPlaceholder: 'Graph view placeholder',
llmWiki: 'LLM Wiki',
skills: 'Skills',
contents: 'Contents',
contents: 'Navigation',
topics: 'Topics',
concept: 'Concept',
entity: 'Entity',
@@ -1650,6 +1650,7 @@ Example: Virtual Hosted Style`,
"Select or customize a specific structure from the Blueprint library on the left to define your Wiki's content framework and visual presentation.",
blueprintsPlaceholderSkip:
'If you have no special requirements, feel free to skip this step, and the system will automatically apply the default structure.',
useBlueprint: 'Use this blueprint',
channelEmptyTip:
'No chat channels added yet. Select one below to connect.',
availableChannels: 'Available channels',
@@ -2055,15 +2056,6 @@ Example: Virtual Hosted Style`,
},
knowledgeCompilation: {
builtinTemplates: 'Built-in template',
kind: {
knowledge_graph: 'Knowledge graph',
artifacts: 'Artifacts',
timeline: 'Timeline',
page_index: 'Page index',
tree: 'Tree',
mind_map: 'Mind map',
empty: 'Empty',
},
},
datasetSkill: {
folders: 'Skills',

View File

@@ -463,7 +463,7 @@ export default {
graphPlaceholder: '图谱视图占位',
llmWiki: 'LLM Wiki',
skills: '技能',
contents: '目录',
contents: '导航',
topics: '主题',
concept: '概念',
createDirectoryFolder: '创建目录',
@@ -1350,6 +1350,7 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
'从左侧的 Blueprint 库中选择或自定义特定结构,以定义您的 Wiki 内容框架和视觉呈现。',
blueprintsPlaceholderSkip:
'如果您没有特殊要求,可以跳过此步骤,系统将自动应用默认结构。',
useBlueprint: '使用此蓝图',
channelEmptyTip: '暂未添加任何聊天渠道,请从下方选择一个进行连接。',
availableChannels: '可用渠道',
availableChannelsDescription: '选择要添加的聊天渠道',
@@ -1714,15 +1715,6 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
},
knowledgeCompilation: {
builtinTemplates: '内置模板',
kind: {
knowledge_graph: '知识图谱',
artifacts: 'Artifacts',
timeline: 'Timeline',
page_index: 'Page index',
tree: '树',
mind_map: '思维导图',
empty: '空',
},
},
datasetSkill: {
folders: '技能',

View File

@@ -1,14 +1,11 @@
import { CirclePause, WandSparkles } from 'lucide-react';
import { CirclePause, Logs, WandSparkles } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IconFontFill } from '@/components/icon-font';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
GenerateType,
IconKeyMap,
} from '@/pages/dataset/dataset/generate-button/constants';
import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants';
import {
ITraceInfo,
useDatasetGenerate,
@@ -79,10 +76,7 @@ export function CompilationEmptyState({
) : (
<div className="w-full max-w-md p-6 flex flex-col gap-4">
<div className="flex items-center gap-2 text-text-primary">
<IconFontFill
name={IconKeyMap[generateType]}
className="text-accent-primary"
/>
<Logs className="size-5" />
<span>{t(LabelKeyMap[type])}</span>
</div>
<div className="flex items-center gap-2">

View File

@@ -15,7 +15,6 @@ import {
useFetchKnowledgeBaseConfiguration,
useFetchKnowledgeGraph,
} from '@/hooks/use-knowledge-request';
import { Routes } from '@/routes';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
@@ -83,10 +82,7 @@ export default function Compilation() {
return (
<section className="flex flex-col p-4 gap-4 h-full">
<header className="space-y-5">
<BackButton
to={`${Routes.DatasetBase}${Routes.Files}/${id}`}
onClick={navigateToDataFile(id!)}
>
<BackButton onClick={navigateToDataFile(id!)}>
{t('common.back')}
</BackButton>

View File

@@ -27,10 +27,3 @@ export const GenerateTypeMap = {
[GenerateType.Artifact]: ProcessingType.artifact,
[GenerateType.ToSkills]: ProcessingType.skill,
};
export const IconKeyMap = {
KnowledgeGraph: 'knowledgegraph',
Raptor: 'dataflow-01',
Artifact: 'book-open-01',
ToSkills: 'spark',
};

View File

@@ -9,7 +9,7 @@ import { startCase } from 'lodash';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FieldLabelKeyMap } from '@/pages/user-setting/compilation-templates/edit-template/utils';
import { FieldLabelKeyMap } from '@/pages/user-setting/compilation-templates/create-next/utils';
import { useAddFieldForm } from '../hooks/use-add-field-form';
@@ -50,6 +50,14 @@ export function AddFieldModal({
onOpenChange(false);
}, [onOpenChange]);
const handleConfirm = useCallback(
(field: Record<string, string>) => {
onAdd(field);
onOpenChange(false);
},
[onAdd, onOpenChange],
);
const getFieldLabel = useCallback(
(key: string) => {
return FieldLabelKeyMap[key] ? t(FieldLabelKeyMap[key]) : startCase(key);
@@ -68,7 +76,7 @@ export function AddFieldModal({
<Button type="button" variant="outline" onClick={handleClose}>
{t('common.cancel')}
</Button>
<Button type="button" onClick={handleSubmit(onAdd)}>
<Button type="button" onClick={handleSubmit(handleConfirm)}>
{t('common.confirm')}
</Button>
</div>

View File

@@ -5,12 +5,13 @@ import { Textarea } from '@/components/ui/textarea';
import { TreeDataItem, TreeView } from '@/components/ui/tree-view';
import { useFetchWikiPresets } from '@/hooks/use-compilation-template-request';
import { IWikiPreset } from '@/interfaces/database/compilation-template';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
import { groupBy } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { Checkbox } from '@/components/ui/checkbox';
type WikiPresetTreeItem = TreeDataItem & {
_preset?: IWikiPreset;
@@ -73,12 +74,34 @@ export function BlueprintsStep({
`templates.${selectedTemplateIndex}.config.instruction` as const;
const pageExamplePath =
`templates.${selectedTemplateIndex}.config.page_example` as const;
const useBlueprintPath =
`templates.${selectedTemplateIndex}.config.use_blueprint` as const;
const pageExample = useWatch({
control: form.control,
name: pageExamplePath,
});
const instruction = useWatch({
control: form.control,
name: instructionPath,
});
const useBlueprint = useWatch({
control: form.control,
name: useBlueprintPath,
});
const selectedTemplateName = useMemo(() => {
if (!selectedItemId) return '';
const selectedItem = treeData
.flatMap((node) => node.children ?? [])
.find((child) => child.id === selectedItemId);
return selectedItem?.name ?? '';
}, [treeData, selectedItemId]);
const hasTemplateData = Boolean(selectedItemId || instruction || pageExample);
const handleSelect = useCallback(
(item?: TreeDataItem) => {
if (!item || item.children) return;
@@ -97,6 +120,14 @@ export function BlueprintsStep({
[form, instructionPath, pageExamplePath],
);
const handleToggleBlueprint = useCallback(
(checked: boolean | 'indeterminate') => {
if (checked === 'indeterminate') return;
form.setValue(useBlueprintPath, checked, { shouldValidate: false });
},
[form, useBlueprintPath],
);
const handlePageExampleChange = useCallback(
(value: string) => {
form.setValue(pageExamplePath, value, { shouldValidate: false });
@@ -120,7 +151,20 @@ export function BlueprintsStep({
</div>
<div className="flex-1 min-h-0 flex flex-col">
{selectedItemId ? (
{hasTemplateData && (
<section className="flex justify-between p-5">
<span>{selectedTemplateName}</span>
<div className="space-x-2">
<Checkbox
id="use-blueprint-checkbox"
checked={Boolean(useBlueprint)}
onCheckedChange={handleToggleBlueprint}
/>
<span>{t('setting.useBlueprint')}</span>
</div>
</section>
)}
{hasTemplateData ? (
<div className="flex-1 min-h-0 flex flex-col p-5 gap-4 overflow-y-auto">
<RAGFlowFormItem
name={instructionPath}
@@ -140,7 +184,7 @@ export function BlueprintsStep({
<BlueprintsEmptyState />
)}
<footer className="shrink-0 px-5 py-4 border-t border-border-button flex items-center justify-between">
<footer className="shrink-0 px-5 py-4 border-t border-border-button flex items-center justify-end gap-5">
<Button type="button" variant="outline" onClick={onBack}>
{t('common.back')}
</Button>

View File

@@ -11,8 +11,8 @@ import {
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { getTypeOptionsFromBuiltinSection } from '@/pages/user-setting/compilation-templates/edit-template/utils';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
import { getTypeOptionsFromBuiltinSection } from '@/pages/user-setting/compilation-templates/create-next/utils';
import { FieldCard } from './field-card';

View File

@@ -12,10 +12,10 @@ import { UseFormReturn, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CompilationTemplateKind } from '@/constants/compilation';
import { TreeTemplateFields } from '@/pages/user-setting/compilation-templates/edit-template/components/tree-template-fields';
import { useTemplateKindChange } from '@/pages/user-setting/compilation-templates/edit-template/hooks/use-template-kind-change';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { SectionTitleKeyMap } from '@/pages/user-setting/compilation-templates/edit-template/utils';
import { TreeTemplateFields } from '@/pages/user-setting/compilation-templates/create-next/components/tree-template-fields';
import { useTemplateKindChange } from '@/pages/user-setting/compilation-templates/create-next/hooks/use-template-kind-change';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
import { SectionTitleKeyMap } from '@/pages/user-setting/compilation-templates/create-next/utils';
import { useActiveSectionTab } from '../hooks/use-active-section-tab';
import { useAvailableKindOptions } from '../hooks/use-available-kind-options';
@@ -233,7 +233,7 @@ export function TemplateConfiguration({
</div>
</div>
<footer className="shrink-0 px-5 py-4 border-t border-border-button flex items-center justify-between">
<footer className="shrink-0 px-5 py-4 border-t border-border-button flex items-center justify-end gap-5">
<Button type="button" variant="outline" onClick={onBack}>
{t('common.back')}
</Button>

View File

@@ -9,8 +9,10 @@ import {
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { DefaultTemplateValues } from '@/pages/user-setting/compilation-templates/edit-template/utils';
import { formatKindLabel } from '@/utils/compilation-template-util';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
import { DefaultTemplateValues } from '@/pages/user-setting/compilation-templates/create-next/utils';
import { useTemplateAddButton } from '../hooks/use-template-add-button';
@@ -86,7 +88,7 @@ function TemplateSidebarItem({
</span>
{template?.kind && (
<span className="ml-2 shrink-0 text-text-secondary">
{t(`knowledgeCompilation.kind.${template.kind}`)}
{formatKindLabel(template.kind)}
</span>
)}
</div>

View File

@@ -3,7 +3,7 @@ import {
createEmptyField,
getFieldKeyOrder,
getTypeOptionsFromBuiltinSection,
} from '@/pages/user-setting/compilation-templates/edit-template/utils';
} from '@/pages/user-setting/compilation-templates/create-next/utils';
import { useCallback, useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';

View File

@@ -1,5 +1,5 @@
import { CompilationTemplateKind } from '@/constants/compilation';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
import { useMemo } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';

View File

@@ -7,7 +7,7 @@ import {
import {
isConfigMetaKey,
sortSectionNames,
} from '@/pages/user-setting/compilation-templates/edit-template/utils';
} from '@/pages/user-setting/compilation-templates/create-next/utils';
export const useBuiltinTemplate = (
builtins: ICompilationTemplateBuiltin[],

View File

@@ -6,21 +6,22 @@ import {
} from '@/hooks/use-compilation-template-group-request';
import { useFetchBuiltinCompilationTemplates } from '@/hooks/use-compilation-template-request';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import { isCreateCompilationTemplateGroup } from '@/utils/compilation-template-util';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { useCompilationTemplateGroupForm } from '@/pages/user-setting/compilation-templates/edit-template/hooks/use-compilation-template-group-form';
import { useCompilationTemplateGroupSubmit } from '@/pages/user-setting/compilation-templates/edit-template/hooks/use-compilation-template-group-submit';
import { formatKindLabel } from '@/utils/compilation-template-util';
import { useCompilationTemplateGroupForm } from '@/pages/user-setting/compilation-templates/create-next/hooks/use-compilation-template-group-form';
import { useCompilationTemplateGroupSubmit } from '@/pages/user-setting/compilation-templates/create-next/hooks/use-compilation-template-group-submit';
export const useCreateNextCompilationTemplateGroup = () => {
const { t } = useTranslation();
const { id } = useParams<{ id: string }>();
const { navigateToCompilationTemplates } = useNavigatePage();
const isCreate = !id || id === 'create';
const isCreate = isCreateCompilationTemplateGroup(id);
const { data: detail } = useFetchCompilationTemplateGroup(id);
const { data: detail } = useFetchCompilationTemplateGroup();
const { data: builtins, kindOptions: builtinKindOptions } =
useFetchBuiltinCompilationTemplates();
const defaultModelDictionary = useFetchDefaultModelDictionary();
@@ -34,9 +35,9 @@ export const useCreateNextCompilationTemplateGroup = () => {
() =>
builtinKindOptions.map((option) => ({
...option,
label: t(`knowledgeCompilation.kind.${option.value}`),
label: formatKindLabel(option.value),
})),
[builtinKindOptions, t],
[builtinKindOptions],
);
const { form } = useCompilationTemplateGroupForm({

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { ArrayPath, UseFormReturn } from 'react-hook-form';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
export const useFieldArrayHandlers = (
form: UseFormReturn<FormSchemaType>,

View File

@@ -1,5 +1,5 @@
import { CompilationTemplateKind } from '@/constants/compilation';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
import { UseFormReturn, useWatch } from 'react-hook-form';
export const useTemplateAddButton = (

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
export function useTemplatePreviewSheets(
form: UseFormReturn<FormSchemaType>,

View File

@@ -5,7 +5,7 @@ import {
ICompilationTemplateBuiltin,
ICompilationTemplateSection,
} from '@/interfaces/database/compilation-template';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/edit-template/schema';
import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
export const useTemplateSectionData = (
form: UseFormReturn<FormSchemaType>,

View File

@@ -12,6 +12,7 @@ import { useCallback, useState } from 'react';
import { useFieldArray, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useFetchCompilationTemplateGroup } from '@/hooks/use-compilation-template-group-request';
import { BasicInfoStep } from './components/basic-info-step';
import { BlueprintsStep } from './components/blueprints-step';
import { TemplateConfiguration } from './components/template-configuration';
@@ -26,6 +27,7 @@ export default function CreateNextCompilationTemplate() {
const { form, kindOptions, builtins, onSubmit, isCreate, isLoading } =
useCreateNextCompilationTemplateGroup();
const { data: group } = useFetchCompilationTemplateGroup();
const { fields, append, remove } = useFieldArray({
control: form.control,
@@ -77,7 +79,7 @@ export default function CreateNextCompilationTemplate() {
<h2 className="font-medium text-text-secondary">
{isCreate
? t('setting.addTemplateGroup')
: t('setting.editTemplateGroup')}
: group?.name || t('setting.editTemplateGroup')}
</h2>
</header>

View File

@@ -16,6 +16,15 @@ export const buildRaptorConfigSchema = (t: (key: string) => string) =>
rechunk: z.boolean().optional(),
});
export const buildSynthesisSchema = () =>
z
.object({
compile_kwd: z.string().optional(),
enabled: z.boolean().optional(),
example: z.string().optional(),
})
.passthrough();
export const buildTemplateSchema = (t: (key: string) => string) =>
z.object({
id: z.string().optional(),
@@ -24,7 +33,13 @@ export const buildTemplateSchema = (t: (key: string) => string) =>
llm_id: z.string().min(1, t('setting.llmForExtractionRequired')),
kind: z.string().min(1, t('setting.templateKindRequired')),
config: z.record(
z.union([buildRaptorConfigSchema(t), buildSectionSchema(t), z.string()]),
z.union([
buildRaptorConfigSchema(t),
buildSectionSchema(t),
buildSynthesisSchema(),
z.string(),
z.boolean(),
]),
),
});

View File

@@ -15,6 +15,20 @@ import { FormSchemaType, TemplateSchemaType } from './schema';
export const DefaultFieldKeys = ['type', 'description', 'rule'];
export const splitExampleToBlueprintFields = (
example: string,
): { instruction: string; page_example: string } => {
const trimmed = example.trim();
const separatorIndex = trimmed.indexOf('\n\n');
if (separatorIndex === -1) {
return { instruction: trimmed, page_example: '' };
}
return {
instruction: trimmed.slice(0, separatorIndex).trim(),
page_example: trimmed.slice(separatorIndex + 2).trim(),
};
};
export const FieldKeyOrders = [
DefaultFieldKeys,
['statement', 'subject'],
@@ -42,6 +56,7 @@ export const DefaultTemplateValues: TemplateSchemaType = {
example: '',
instruction: '',
page_example: '',
use_blueprint: false,
},
};
@@ -60,6 +75,8 @@ export const isConfigMetaKey = (key: string) =>
'example',
'instruction',
'page_example',
'synthesis',
'use_blueprint',
].includes(key);
export const createEmptyField = (keys: string[]) =>
@@ -87,6 +104,10 @@ export const buildConfigFromBuiltin = (
kind: string,
llmId: string,
): TemplateSchemaType['config'] => {
const example =
typeof builtinTemplate.config?.example === 'string'
? builtinTemplate.config.example
: '';
const sections: TemplateSchemaType['config'] = {
kind,
llm_id: llmId,
@@ -98,8 +119,24 @@ export const buildConfigFromBuiltin = (
typeof builtinTemplate.config?.example === 'string'
? builtinTemplate.config.example
: '',
...(typeof builtinTemplate.config?.synthesis === 'object' &&
builtinTemplate.config?.synthesis !== null
? {
synthesis: builtinTemplate.config
.synthesis as TemplateSchemaType['config']['synthesis'],
}
: {}),
use_blueprint:
kind === CompilationTemplateKind.Artifacts && example.length > 0,
};
if (kind === CompilationTemplateKind.Artifacts && example.length > 0) {
const { instruction, page_example } =
splitExampleToBlueprintFields(example);
sections.instruction = instruction;
sections.page_example = page_example;
}
if (kind === CompilationTemplateKind.Tree) {
const builtinRaptor: ICompilationTemplateRaptorConfig =
builtinTemplate.config?.raptor ?? {};
@@ -128,13 +165,29 @@ export const transformDetailToForm = (
detail: ICompilationTemplate,
): TemplateSchemaType => {
const config = detail.config ?? {};
const example = typeof config.example === 'string' ? config.example : '';
const base: TemplateSchemaType['config'] = {
kind: config.kind ?? '',
llm_id: config.llm_id ?? '',
global_rules: config.global_rules ?? '',
example: typeof config.example === 'string' ? config.example : '',
...(typeof config.synthesis === 'object' && config.synthesis !== null
? {
synthesis:
config.synthesis as TemplateSchemaType['config']['synthesis'],
}
: {}),
use_blueprint:
detail.kind === CompilationTemplateKind.Artifacts && example.length > 0,
};
if (detail.kind === CompilationTemplateKind.Artifacts && example.length > 0) {
const { instruction, page_example } =
splitExampleToBlueprintFields(example);
base.instruction = instruction;
base.page_example = page_example;
}
if (detail.kind === CompilationTemplateKind.Tree) {
const raptor: ICompilationTemplateRaptorConfig = config.raptor ?? {};
return {
@@ -196,6 +249,10 @@ export const transformTemplateToPayload = (template: TemplateSchemaType) => {
Object.entries(template.config).forEach(([key, value]) => {
if (key === 'kind' || key === 'llm_id') return;
if (key === 'instruction' || key === 'page_example') return;
if (key === 'synthesis') {
config[key] = value as ICompilationTemplateConfigRequest[string];
return;
}
if (isConfigMetaKey(key)) {
if (typeof value === 'string') config[key] = value;
} else {
@@ -204,9 +261,13 @@ export const transformTemplateToPayload = (template: TemplateSchemaType) => {
});
if (template.kind === CompilationTemplateKind.Artifacts) {
const instruction = String(template.config.instruction ?? '').trim();
const pageExample = String(template.config.page_example ?? '').trim();
config.example = [instruction, pageExample].filter(Boolean).join('\n\n');
if (template.config.use_blueprint) {
const instruction = String(template.config.instruction ?? '').trim();
const pageExample = String(template.config.page_example ?? '').trim();
config.example = [instruction, pageExample].filter(Boolean).join('\n\n');
} else {
config.example = '';
}
}
return {

View File

@@ -1,174 +0,0 @@
import { Collapse } from '@/components/collapse';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { ICompilationTemplateSection } from '@/interfaces/database/compilation-template';
import { startCase } from 'lodash';
import { Plus, Trash2 } from 'lucide-react';
import { useMemo } from 'react';
import {
ArrayPath,
Path,
useFieldArray,
useFormContext,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useFieldTypeChange } from '../hooks/use-field-type-change';
import { FormSchemaType } from '../schema';
import { createEmptyField, FieldLabelKeyMap } from '../utils';
type FieldsSectionProps = {
name: Path<FormSchemaType>;
title: string;
fieldKeys: string[];
defaultOpen?: boolean;
builtinSection?: ICompilationTemplateSection;
};
export function FieldsSection({
name,
title,
fieldKeys,
defaultOpen = false,
builtinSection,
}: FieldsSectionProps) {
const { t } = useTranslation();
const form = useFormContext<FormSchemaType>();
const fieldsPath = `${name}.fields` as ArrayPath<FormSchemaType>;
const { fields, append, remove } = useFieldArray({
name: fieldsPath,
control: form.control,
});
const typeOptions = useMemo(() => {
const typeSet = new Set<string>();
builtinSection?.fields?.forEach((field) => {
if (field.type) typeSet.add(field.type);
});
return Array.from(typeSet)
.sort()
.map((value) => ({ label: value, value }));
}, [builtinSection]);
return (
<Collapse defaultOpen={defaultOpen} title={title}>
<section className="space-y-4">
<RAGFlowFormItem
name={`${name}.description`}
label={t('setting.description')}
>
<Textarea
placeholder={t('setting.descriptionPlaceholder')}
rows={3}
/>
</RAGFlowFormItem>
<section className="space-y-3">
{fields.map((field, index) => (
<Card
key={field.id}
className="border-border-button bg-transparent transition-colors has-[button:hover]:border-state-error"
>
<CardContent className="p-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-text-secondary">
{t('setting.field')} #{index + 1}
</span>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(index)}
className="text-text-secondary hover:text-state-error"
>
<Trash2 className="size-4" />
</Button>
)}
</div>
{fieldKeys.map((key) => (
<FieldFormItem
key={key}
fieldKey={key}
fieldName={`${fieldsPath}.${index}.${key}`}
typeOptions={typeOptions}
builtinSection={builtinSection}
fieldsPath={fieldsPath}
index={index}
/>
))}
</CardContent>
</Card>
))}
</section>
<Button
type="button"
variant="outline"
onClick={() => append(createEmptyField(fieldKeys))}
>
<Plus className="size-4 mr-2" />
{t('setting.addField')}
</Button>
</section>
</Collapse>
);
}
type FieldFormItemProps = {
fieldKey: string;
fieldName: string;
typeOptions: { label: string; value: string }[];
builtinSection?: ICompilationTemplateSection;
fieldsPath: ArrayPath<FormSchemaType>;
index: number;
};
function FieldFormItem({
fieldKey,
fieldName,
typeOptions,
builtinSection,
fieldsPath,
index,
}: FieldFormItemProps) {
const { t } = useTranslation();
const form = useFormContext<FormSchemaType>();
const handleTypeChange = useFieldTypeChange({
form,
builtinSection,
fieldsPath: fieldsPath as `templates.${number}.config.${string}.fields`,
index,
});
return (
<RAGFlowFormItem
name={fieldName}
label={
FieldLabelKeyMap[fieldKey]
? t(FieldLabelKeyMap[fieldKey])
: startCase(fieldKey)
}
required
>
{fieldKey === 'type' ? (
(field) => (
<SelectWithSearch
options={typeOptions}
value={field.value}
onChange={(value) => handleTypeChange(field, value)}
disabled={field.disabled}
placeholder={t('common.selectPlaceholder')}
/>
)
) : (
<Textarea placeholder={t('setting.descriptionPlaceholder')} rows={2} />
)}
</RAGFlowFormItem>
);
}

View File

@@ -1,213 +0,0 @@
import { Collapse } from '@/components/collapse';
import { ModelTreeSelectFormField } from '@/components/model-tree-select';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import {
ICompilationTemplateBuiltin,
ICompilationTemplateSection,
} from '@/interfaces/database/compilation-template';
import { Trash2 } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CompilationTemplateKind } from '@/constants/compilation';
import { useTemplateKindChange } from '../hooks/use-template-kind-change';
import { FormSchemaType } from '../schema';
import {
DefaultFieldKeys,
getFieldKeyOrder,
isConfigMetaKey,
SectionTitleKeyMap,
sortSectionNames,
} from '../utils';
import { FieldsSection } from './fields-section';
import { TreeTemplateFields } from './tree-template-fields';
type TemplateCardProps = {
index: number;
form: UseFormReturn<FormSchemaType>;
kindOptions: { label: string; value: string }[];
builtins: ICompilationTemplateBuiltin[];
onRemove: (index: number) => void;
canRemove: boolean;
};
export function TemplateCard({
index,
form,
kindOptions,
builtins,
onRemove,
canRemove,
}: TemplateCardProps) {
const { t } = useTranslation();
const kind = useWatch({
control: form.control,
name: `templates.${index}.kind`,
});
const builtinTemplate = useMemo(
() => builtins.find((template) => template.kind === kind),
[builtins, kind],
);
const sectionNames = useMemo(() => {
const names = Object.keys(builtinTemplate?.config ?? {}).filter(
(key) => !isConfigMetaKey(key),
);
return sortSectionNames(names);
}, [builtinTemplate]);
const handleRemove = useCallback(() => {
onRemove(index);
}, [index, onRemove]);
const handleRemoveClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleRemove();
},
[handleRemove],
);
const handleKindChange = useTemplateKindChange({
form,
index,
builtins,
});
return (
<Card className="border-border-button">
<CardContent className="p-4">
<Collapse
title={
<div>
<span className="text-sm font-medium text-text-primary">
{t('setting.template')} #{index + 1}
</span>
<span className="ml-2 text-xs text-text-secondary">
{kindOptions.find((option) => option.value === kind)?.label}
</span>
</div>
}
rightContent={
canRemove && (
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={handleRemoveClick}
className="text-text-secondary hover:text-state-error"
>
<Trash2 className="size-4" />
</Button>
)
}
defaultOpen
>
<div className="space-y-4">
{kind !== CompilationTemplateKind.Tree && (
<RAGFlowFormItem
name={`templates.${index}.name`}
label={t('setting.templateName')}
>
<Input placeholder={t('common.namePlaceholder')} />
</RAGFlowFormItem>
)}
<RAGFlowFormItem
name={`templates.${index}.description`}
label={t('setting.templateDescription')}
>
<Textarea
placeholder={t('common.descriptionPlaceholder')}
rows={2}
/>
</RAGFlowFormItem>
<ModelTreeSelectFormField
name={`templates.${index}.llm_id`}
label={t('setting.llmForExtraction')}
required
/>
<RAGFlowFormItem
name={`templates.${index}.kind`}
label={t('knowledgeCompilation.builtinTemplates')}
required
>
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(value) => handleKindChange(field, value)}
disabled={field.disabled}
options={kindOptions}
placeholder={t('common.selectPlaceholder')}
/>
)}
</RAGFlowFormItem>
{kind === CompilationTemplateKind.Artifacts && (
<RAGFlowFormItem
name={`templates.${index}.config.example`}
label={t('setting.example')}
>
<Textarea
placeholder={t('setting.examplePlaceholder')}
rows={4}
/>
</RAGFlowFormItem>
)}
{kind === CompilationTemplateKind.Tree ? (
<TreeTemplateFields index={index} />
) : (
sectionNames.map((sectionName, sectionIndex) => {
const builtinSection = builtinTemplate?.config?.[sectionName];
const firstField = (
builtinSection as ICompilationTemplateSection | undefined
)?.fields?.[0];
const firstFieldKeys = firstField
? Object.keys(firstField)
: DefaultFieldKeys;
const fieldKeys = getFieldKeyOrder(firstFieldKeys);
return (
<FieldsSection
key={sectionName}
name={
`templates.${index}.config.${sectionName}` as `templates.${number}.config.${string}`
}
title={t(SectionTitleKeyMap[sectionName] ?? sectionName)}
fieldKeys={fieldKeys}
defaultOpen={sectionIndex === 0}
builtinSection={
builtinSection as ICompilationTemplateSection | undefined
}
/>
);
})
)}
<RAGFlowFormItem
name={`templates.${index}.config.global_rules`}
label={t('setting.globalRules')}
>
<Textarea
placeholder={t('setting.globalRulesPlaceholder')}
rows={4}
/>
</RAGFlowFormItem>
</div>
</Collapse>
</CardContent>
</Card>
);
}

View File

@@ -1,49 +0,0 @@
import {
useCreateCompilationTemplateGroup,
useFetchCompilationTemplateGroup,
useUpdateCompilationTemplateGroup,
} from '@/hooks/use-compilation-template-group-request';
import { useFetchBuiltinCompilationTemplates } from '@/hooks/use-compilation-template-request';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
export const useCompilationTemplateGroupData = () => {
const { id } = useParams<{ id: string }>();
const isCreate = id === 'create';
const { t } = useTranslation();
const { data: detail } = useFetchCompilationTemplateGroup(id);
const { data: builtins, kindOptions: builtinKindOptions } =
useFetchBuiltinCompilationTemplates();
const defaultModelDictionary = useFetchDefaultModelDictionary();
const { createGroup, loading: createLoading } =
useCreateCompilationTemplateGroup();
const { updateGroup, loading: updateLoading } =
useUpdateCompilationTemplateGroup();
const kindOptions = useMemo(
() =>
builtinKindOptions.map((option) => ({
...option,
label: t(`knowledgeCompilation.kind.${option.value}`),
})),
[builtinKindOptions, t],
);
const isLoading = createLoading || updateLoading;
return {
id,
isCreate,
detail,
builtins,
kindOptions,
defaultModelDictionary,
createGroup,
updateGroup,
isLoading,
};
};

View File

@@ -1,51 +0,0 @@
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useWatch } from 'react-hook-form';
import { DefaultValues } from '../utils';
import { useCompilationTemplateGroupData } from './use-compilation-template-group-data';
import { useCompilationTemplateGroupForm } from './use-compilation-template-group-form';
import { useCompilationTemplateGroupSubmit } from './use-compilation-template-group-submit';
export const useEditCompilationTemplateGroup = () => {
const {
id,
isCreate,
detail,
builtins,
kindOptions,
defaultModelDictionary,
createGroup,
updateGroup,
isLoading,
} = useCompilationTemplateGroupData();
const { navigateToCompilationTemplates } = useNavigatePage();
const { form } = useCompilationTemplateGroupForm({
detail,
defaultLlmId: defaultModelDictionary.llm_id,
isCreate,
});
const watchedValues = useWatch({ control: form.control }) ?? DefaultValues;
const { onSubmit } = useCompilationTemplateGroupSubmit({
isCreate,
id,
createGroup,
updateGroup,
onSuccess: navigateToCompilationTemplates,
});
return {
id,
isCreate,
form,
watchedValues,
kindOptions,
builtins,
onSubmit,
isLoading,
navigateToCompilationTemplates,
};
};

View File

@@ -1,66 +0,0 @@
import {
ICompilationTemplateField,
ICompilationTemplateSection,
} from '@/interfaces/database/compilation-template';
import { useCallback } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { FormSchemaType } from '../schema';
type FieldLike = {
value: string;
onChange: (value: string) => void;
};
type UseFieldTypeChangeOptions = {
form: UseFormReturn<FormSchemaType>;
builtinSection?: ICompilationTemplateSection;
fieldsPath: `templates.${number}.config.${string}.fields`;
index: number;
};
const FIELD_SYNC_KEYS: Array<keyof ICompilationTemplateField> = [
'description',
'rule',
];
export const useFieldTypeChange = ({
form,
builtinSection,
fieldsPath,
index,
}: UseFieldTypeChangeOptions) => {
return useCallback(
(field: FieldLike, value: string) => {
if (!value || value === field.value) {
field.onChange(value);
return;
}
const matchedField = builtinSection?.fields?.find(
(builtinField) => builtinField.type === value,
);
const currentField = form.getValues(`${fieldsPath}.${index}`);
const nextField: Record<string, string> = {
...currentField,
type: value,
};
if (matchedField) {
FIELD_SYNC_KEYS.forEach((key) => {
if (key in matchedField) {
nextField[key] = matchedField[key] ?? '';
}
});
}
form.setValue(`${fieldsPath}.${index}`, nextField, {
shouldValidate: false,
shouldDirty: true,
shouldTouch: true,
});
},
[builtinSection, fieldsPath, form, index],
);
};

View File

@@ -1,183 +0,0 @@
import BackButton from '@/components/back-button';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import { Routes } from '@/routes';
import { Plus } from 'lucide-react';
import { useMemo } from 'react';
import { useFieldArray, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import JsonView from 'react18-json-view';
import { CompilationTemplateKind } from '@/constants/compilation';
import { TemplateCard } from './components/template-card';
import { useEditCompilationTemplateGroup } from './hooks/use-edit-compilation-template-group';
import { DefaultTemplateValues } from './utils';
export default function EditCompilationTemplate() {
const { t } = useTranslation();
const {
isCreate,
form,
watchedValues,
kindOptions,
builtins,
onSubmit,
isLoading,
navigateToCompilationTemplates,
} = useEditCompilationTemplateGroup();
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'templates',
});
const watchedTemplates = useWatch({
control: form.control,
name: 'templates',
});
const isAddTemplateDisabled = watchedTemplates?.some(
(template) => template.kind === CompilationTemplateKind.Artifacts,
);
const hasNonArtifactsTemplate = watchedTemplates?.some(
(template) =>
template.kind && template.kind !== CompilationTemplateKind.Artifacts,
);
const availableKindOptions = useMemo(
() =>
hasNonArtifactsTemplate
? kindOptions.filter(
(option) => option.value !== CompilationTemplateKind.Artifacts,
)
: kindOptions,
[hasNonArtifactsTemplate, kindOptions],
);
const handleAddTemplate = () => {
const firstTemplateLlmId = form.getValues('templates.0.llm_id');
append({
...DefaultTemplateValues,
llm_id: firstTemplateLlmId || '',
});
};
return (
<section className="h-full flex flex-col">
<header className="shrink-0 px-5 py-4 border-b border-border-button flex flex-col items-start gap-2">
<BackButton
to={`${Routes.UserSetting}${Routes.CompilationTemplates}`}
/>
<h2 className="text-xl font-medium text-text-primary">
{isCreate
? t('setting.addTemplateGroup')
: t('setting.editTemplateGroup')}
</h2>
</header>
<div className="flex-1 min-h-0 flex">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex-1 min-w-0 flex flex-col min-h-0"
>
<div className="flex-1 min-h-0 overflow-y-auto p-5">
<div className="max-w-2xl space-y-6 mx-auto">
<RAGFlowFormItem
name="name"
label={t('setting.groupName')}
required
>
<Input placeholder={t('common.namePlaceholder')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name="description"
label={t('setting.groupDescription')}
>
<Textarea
placeholder={t('common.descriptionPlaceholder')}
rows={3}
/>
</RAGFlowFormItem>
<section className="space-y-4">
{fields.map((field, index) => (
<TemplateCard
key={field.id}
index={index}
form={form}
kindOptions={availableKindOptions}
builtins={builtins}
onRemove={remove}
canRemove={fields.length > 1}
/>
))}
</section>
<Button
type="button"
variant="outline"
onClick={handleAddTemplate}
disabled={isAddTemplateDisabled}
>
<Plus className="size-4 mr-2" />
{t('setting.addTemplate')}
</Button>
</div>
</div>
<div className="shrink-0 px-5 py-4 border-t border-border-button flex items-center justify-end gap-3">
<Button
type="button"
variant="outline"
onClick={navigateToCompilationTemplates}
disabled={isLoading}
>
{t('common.cancel')}
</Button>
<Button type="submit" loading={isLoading} disabled={isLoading}>
{t('common.confirm')}
</Button>
</div>
</form>
</Form>
<aside className="flex-1 min-w-0 border-l border-border-button p-5 hidden lg:flex flex-col">
<Tabs defaultValue="json" className="h-full flex flex-col min-h-0">
<TabsList className="mb-3 self-start shrink-0">
<TabsTrigger value="json">JSON</TabsTrigger>
<TabsTrigger value="processFlow">
{t('setting.processFlow')}
</TabsTrigger>
</TabsList>
<TabsContent
value="json"
className="mt-0 flex-1 min-h-0 overflow-y-auto"
>
<JsonView
src={watchedValues}
displaySize
collapseStringsAfterLength={100000000000}
className="w-full h-full break-words p-2 bg-muted rounded-md"
/>
</TabsContent>
<TabsContent
value="processFlow"
className="mt-0 flex-1 min-h-0 overflow-y-auto"
>
<div className="w-full h-full flex items-center justify-center text-sm text-text-secondary">
{t('setting.processFlowComingSoon')}
</div>
</TabsContent>
</Tabs>
</aside>
</div>
</section>
);
}

View File

@@ -6,7 +6,8 @@ import { CompilationTemplateScope } from '@/constants/compilation';
import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template';
import { Database, FileText, LucideIcon } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { formatKindLabel } from '@/utils/compilation-template-util';
import { TemplateDropdown } from './template-dropdown';
type TemplateCardProps = {
@@ -27,8 +28,6 @@ function ScopeIcon({ scope }: { scope?: string }) {
}
export function TemplateCard({ data, onClick, onDelete }: TemplateCardProps) {
const { t } = useTranslation();
const kinds = useMemo(
() => Array.from(new Set((data.templates ?? []).map((item) => item.kind))),
[data.templates],
@@ -64,7 +63,7 @@ export function TemplateCard({ data, onClick, onDelete }: TemplateCardProps) {
<div className="flex flex-wrap gap-2 mt-2">
{kinds.map((kind) => (
<Badge key={kind} variant="secondary">
{t(`knowledgeCompilation.kind.${kind}`, kind)}
{formatKindLabel(kind)}
</Badge>
))}
</div>

View File

@@ -44,7 +44,6 @@ export enum Routes {
Model = '/model',
Prompt = '/prompt',
CompilationTemplates = '/compilation-templates',
CompilationTemplate = `${CompilationTemplates}/:id`,
CompilationTemplatesCreateNext = '/compilation-templates/create-next',
DataSource = '/data-source',
DataSourceDetailPage = '/data-source-detail-page',
@@ -318,12 +317,6 @@ const routeConfigOptions = [
},
],
},
{
path: Routes.CompilationTemplate,
layout: false,
Component: () =>
import('@/pages/user-setting/compilation-templates/edit-template'),
},
{
path: Routes.CompilationTemplatesCreateNext,
layout: false,

View File

@@ -0,0 +1,10 @@
import { capitalize, lowerCase } from 'lodash';
export function formatKindLabel(kind: string): string {
return capitalize(lowerCase(kind));
}
export const isCreateCompilationTemplateGroup = (
id?: string,
): id is undefined => {
return !id;
};