Feat: Search for knowledge-base-level graph nodes. (#17444)

This commit is contained in:
balibabu
2026-07-27 21:03:30 +08:00
committed by GitHub
parent 1ae1dd1edb
commit 1cce9b64f1
98 changed files with 3614 additions and 402 deletions

View File

@@ -91,7 +91,7 @@ function TemplateSidebarItem({
</span>
{template?.kind && (
<span className="ml-2 shrink-0 text-text-secondary">
{formatKindLabel(template.kind)}
{formatKindLabel(t, template.kind)}
</span>
)}
</div>

View File

@@ -8,6 +8,7 @@ import { useFetchBuiltinCompilationTemplates } from '@/hooks/use-compilation-tem
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 { formatKindLabel } from '@/utils/compilation-template-util';
@@ -18,6 +19,7 @@ import { useCompilationTemplateGroupSubmit } from '@/pages/user-setting/compilat
export const useCreateNextCompilationTemplateGroup = () => {
const { id } = useParams<{ id: string }>();
const { navigateToCompilationTemplates } = useNavigatePage();
const { t } = useTranslation();
const isCreate = isCreateCompilationTemplateGroup(id);
@@ -35,9 +37,9 @@ export const useCreateNextCompilationTemplateGroup = () => {
() =>
builtinKindOptions.map((option) => ({
...option,
label: formatKindLabel(option.value),
label: formatKindLabel(t, option.value),
})),
[builtinKindOptions],
[builtinKindOptions, t],
);
const { form } = useCompilationTemplateGroupForm({

View File

@@ -0,0 +1,118 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { Modal } from '@/components/ui/modal/modal';
import { Textarea } from '@/components/ui/textarea';
import { ICompilationTemplateSection } from '@/interfaces/database/compilation-template';
import { startCase } from 'lodash';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FieldLabelKeyMap } from '../utils';
import { useAddFieldForm } from '../hooks/use-add-field-form';
type AddFieldModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
sectionName: string;
builtinSection?: ICompilationTemplateSection;
initialField?: Record<string, string>;
onAdd: (field: Record<string, string>) => void;
};
export function AddFieldModal({
open,
onOpenChange,
sectionName,
builtinSection,
initialField,
onAdd,
}: AddFieldModalProps) {
const { t } = useTranslation();
const {
form,
fieldKeys,
hasTypeField,
typeOptions,
handleTypeChange,
handleSubmit,
} = useAddFieldForm({
open,
builtinSection,
initialField,
});
const nonTypeKeys = fieldKeys.filter((key) => key !== 'type');
const handleClose = useCallback(() => {
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);
},
[t],
);
return (
<Modal
open={open}
onOpenChange={onOpenChange}
title={`${initialField ? t('setting.editFieldModalTitle') : t('setting.addFieldModalTitle')} - ${startCase(sectionName)}`}
size="default"
footer={
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={handleClose}>
{t('common.cancel')}
</Button>
<Button type="button" onClick={handleSubmit(handleConfirm)}>
{t('common.confirm')}
</Button>
</div>
}
>
<Form {...form}>
<div className="space-y-4">
{hasTypeField && (
<RAGFlowFormItem name="type" label={getFieldLabel('type')}>
{(field) => (
<SelectWithSearch
{...field}
options={typeOptions}
allowClear
onChange={(value) => {
field.onChange(value);
handleTypeChange(value);
}}
placeholder={t('setting.selectFieldType')}
allowCustomValue
/>
)}
</RAGFlowFormItem>
)}
{nonTypeKeys.map((key) => (
<RAGFlowFormItem key={key} name={key} label={getFieldLabel(key)}>
<Textarea
placeholder={t('setting.descriptionPlaceholder')}
rows={key === 'description' ? 4 : 10}
resize="vertical"
/>
</RAGFlowFormItem>
))}
</div>
</Form>
</Modal>
);
}

View File

@@ -0,0 +1,72 @@
import { Collapse } from '@/components/collapse';
import MarkdownEditor from '@/components/markdown-editor';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Textarea } from '@/components/ui/textarea';
import { useFetchWikiPresets } from '@/hooks/use-compilation-template-request';
import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBlueprintSelection } from '../hooks/use-blueprint-selection';
import { FormSchemaType } from '../schema';
type BlueprintSectionProps = {
form: UseFormReturn<FormSchemaType>;
selectedTemplateIndex: number;
};
export function BlueprintSection({
form,
selectedTemplateIndex,
}: BlueprintSectionProps) {
const { t } = useTranslation();
const { data: presets } = useFetchWikiPresets();
const {
selectedValue,
options,
handleSelect,
instructionPath,
pageExample,
handlePageExampleChange,
} = useBlueprintSelection({ form, selectedTemplateIndex, presets });
if (presets.length === 0) {
return null;
}
return (
<section className="space-y-4 pt-4">
<Collapse
defaultOpen
title={
<h3 className="text-base font-medium">{t('setting.blueprints')}</h3>
}
>
<div className="space-y-4">
<SelectWithSearch
value={selectedValue}
onChange={handleSelect}
options={options}
placeholder={t('common.selectPlaceholder')}
/>
<div className="space-y-4">
<RAGFlowFormItem
name={instructionPath}
label={t('setting.instruction')}
>
<Textarea rows={6} />
</RAGFlowFormItem>
<div className="flex h-[50vh] min-h-0 flex-col">
<MarkdownEditor
content={String(pageExample ?? '')}
onChange={handlePageExampleChange}
/>
</div>
</div>
</div>
</Collapse>
</section>
);
}

View File

@@ -0,0 +1,54 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Pencil, Trash2 } from 'lucide-react';
type FieldCardProps = {
title?: string;
field: Record<string, string>;
onEdit: () => void;
onDelete: () => void;
};
export function FieldCard({ title, field, onEdit, onDelete }: FieldCardProps) {
return (
<Card className="border-border-button bg-transparent group">
<CardContent className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="space-y-2 space-x-2 flex-1 min-w-0">
{title && (
<span className="text-sm font-medium text-text-primary">
{title}
</span>
)}
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={onEdit}
className="text-text-secondary hover:text-text-primary"
>
<Pencil className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={onDelete}
className="text-text-secondary hover:text-state-error"
>
<Trash2 className="size-4" />
</Button>
</div>
</div>
{field.description && (
<p className="text-sm text-text-secondary line-clamp-3">
{field.description}
</p>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,62 @@
import { useTranslation } from 'react-i18next';
import JsonEditor from '@/components/json-edit';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Braces } from 'lucide-react';
interface JsonPreviewSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
value: Record<string, unknown>;
}
export function JsonPreviewSheet({
open,
onOpenChange,
value,
}: JsonPreviewSheetProps) {
const { t } = useTranslation();
return (
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
<Tooltip>
<TooltipTrigger asChild>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<Braces className="size-4" />
</Button>
</SheetTrigger>
</TooltipTrigger>
<TooltipContent>{t('setting.jsonPreview')}</TooltipContent>
</Tooltip>
<SheetContent
className="w-1/2 max-w-[700px] flex flex-col"
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader>
<SheetTitle>{t('setting.jsonPreview')}</SheetTitle>
</SheetHeader>
<div className="flex-1 min-h-0 mt-4">
<JsonEditor
value={value}
height="100%"
options={{ mode: 'tree', modes: ['tree', 'code'] }}
defaultExpanded
/>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,72 @@
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FieldCard } from './field-card';
type SectionFieldGridProps = {
fieldsPath: string;
sectionName: string;
onOpenAddField: () => void;
onEditField: (index: number) => void;
};
export function SectionFieldGrid({
fieldsPath,
sectionName,
onOpenAddField,
onEditField,
}: SectionFieldGridProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove } = useFieldArray({
control: form.control,
name: fieldsPath,
});
const isTypedSection = sectionName === 'entity' || sectionName === 'relation';
const currentFields = useWatch({
control: form.control,
name: fieldsPath,
}) as Record<string, string>[] | undefined;
return (
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{fields.map((field, index) => {
const fieldValue = currentFields?.[index] ?? {};
return (
<FieldCard
key={field.id}
title={isTypedSection ? fieldValue.type : undefined}
field={fieldValue}
onEdit={() => onEditField(index)}
onDelete={() => remove(index)}
/>
);
})}
<Card
role="button"
tabIndex={0}
onClick={onOpenAddField}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onOpenAddField();
}
}}
className={cn(
'border-border-button bg-transparent border-dashed flex flex-col items-center justify-center gap-2 min-h-[140px] cursor-pointer',
'hover:border-border-accent hover:text-text-primary text-text-secondary',
)}
>
<CardContent className="flex flex-col items-center justify-center gap-2 p-4">
<Plus className="size-6" />
<span className="text-sm font-medium">{t('setting.addField')}</span>
</CardContent>
</Card>
</section>
);
}

View File

@@ -0,0 +1,242 @@
import { ModelTreeSelectFormField } from '@/components/model-tree-select';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import { ICompilationTemplateBuiltin } from '@/interfaces/database/compilation-template';
import { startCase } from 'lodash';
import { ReactNode, useCallback } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CompilationTemplateKind } from '@/constants/compilation';
import { TreeTemplateFields } from './tree-template-fields';
import { useTemplateKindChange } from '../hooks/use-template-kind-change';
import { FormSchemaType } from '../schema';
import { SectionTitleKeyMap } from '../utils';
import { useActiveSectionTab } from '../hooks/use-active-section-tab';
import { useAvailableKindOptions } from '../hooks/use-available-kind-options';
import { useBuiltinTemplate } from '../hooks/use-builtin-template';
import { useFieldArrayHandlers } from '../hooks/use-field-array-handlers';
import { useFieldModal } from '../hooks/use-field-modal';
import { useTemplatePreviewSheets } from '../hooks/use-template-preview-sheets';
import { useTemplateSectionData } from '../hooks/use-template-section-data';
import { AddFieldModal } from './add-field-modal';
import { SectionFieldGrid } from './section-field-grid';
import { TemplatePreviewHeader } from './template-preview-header';
type TemplateConfigurationProps = {
form: UseFormReturn<FormSchemaType>;
builtins: ICompilationTemplateBuiltin[];
kindOptions: { label: string; value: string }[];
selectedTemplateIndex: number;
children?: ReactNode;
};
export function TemplateConfiguration({
form,
builtins,
kindOptions,
selectedTemplateIndex,
children,
}: TemplateConfigurationProps) {
const { t } = useTranslation();
const {
addFieldModalOpen,
editingFieldIndex,
setEditingFieldIndex,
handleModalOpenChange,
handleOpenAddField,
handleOpenEditField,
} = useFieldModal();
const kind = useWatch({
control: form.control,
name: `templates.${selectedTemplateIndex}.kind`,
});
const availableKindOptions = useAvailableKindOptions(
form,
kindOptions,
selectedTemplateIndex,
);
const { builtinTemplate, sectionNames } = useBuiltinTemplate(builtins, kind);
const {
jsonSheetOpen,
setJsonSheetOpen,
workflowSheetOpen,
setWorkflowSheetOpen,
allFormValues,
templateName,
} = useTemplatePreviewSheets(form, selectedTemplateIndex);
const { activeSectionTab, setActiveSectionTab } =
useActiveSectionTab(sectionNames);
const handleKindChange = useTemplateKindChange({
form,
index: selectedTemplateIndex,
builtins,
});
const { activeFieldsPath, builtinSection, editingField } =
useTemplateSectionData(
form,
selectedTemplateIndex,
activeSectionTab,
builtinTemplate,
editingFieldIndex,
);
const { handleAddField } = useFieldArrayHandlers(
form,
activeFieldsPath,
editingFieldIndex,
setEditingFieldIndex,
);
const renderSectionTabs = useCallback(
(sectionName: string) => {
return (
sectionName === activeSectionTab && (
<SectionFieldGrid
key={activeFieldsPath}
fieldsPath={activeFieldsPath}
sectionName={sectionName}
onOpenAddField={handleOpenAddField}
onEditField={handleOpenEditField}
/>
)
);
},
[
activeFieldsPath,
activeSectionTab,
handleOpenAddField,
handleOpenEditField,
],
);
return (
<>
<TemplatePreviewHeader
templateName={templateName}
jsonSheetOpen={jsonSheetOpen}
onJsonSheetOpenChange={setJsonSheetOpen}
workflowSheetOpen={workflowSheetOpen}
onWorkflowSheetOpenChange={setWorkflowSheetOpen}
allFormValues={allFormValues}
/>
<div className="flex-1 min-h-0 overflow-y-auto p-5">
<div className="max-w-4xl mx-auto space-y-6">
<RAGFlowFormItem
name={`templates.${selectedTemplateIndex}.name`}
label={t('setting.templateName')}
required
>
<Input placeholder={t('common.namePlaceholder')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name={`templates.${selectedTemplateIndex}.description`}
label={t('setting.templateDescription')}
>
<Textarea
placeholder={t('common.descriptionPlaceholder')}
rows={2}
resize="vertical"
/>
</RAGFlowFormItem>
<ModelTreeSelectFormField
name={`templates.${selectedTemplateIndex}.llm_id`}
label={t('setting.llmForExtraction')}
required
/>
<RAGFlowFormItem
name={`templates.${selectedTemplateIndex}.kind`}
label={t('knowledgeCompilation.builtinTemplates')}
required
>
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(value) => handleKindChange(field, value)}
disabled={field.disabled}
options={availableKindOptions}
placeholder={t('common.selectPlaceholder')}
/>
)}
</RAGFlowFormItem>
<RAGFlowFormItem
name={`templates.${selectedTemplateIndex}.config.global_rules`}
label={t('setting.globalRules')}
>
<Textarea
placeholder={t('setting.globalRulesPlaceholder')}
rows={8}
resize="vertical"
/>
</RAGFlowFormItem>
{kind === CompilationTemplateKind.Tree ? (
<TreeTemplateFields index={selectedTemplateIndex} />
) : (
sectionNames.length > 0 &&
activeSectionTab && (
<Tabs
value={activeSectionTab}
onValueChange={setActiveSectionTab}
className="w-full"
>
<TabsList className="w-full justify-start">
{sectionNames.map((sectionName) => (
<TabsTrigger
key={sectionName}
value={sectionName}
className="flex-1"
>
{t(
SectionTitleKeyMap[sectionName] ??
startCase(sectionName),
)}
</TabsTrigger>
))}
</TabsList>
{sectionNames.map((sectionName) => (
<TabsContent
key={sectionName}
value={sectionName}
className="mt-4"
>
{renderSectionTabs(sectionName)}
</TabsContent>
))}
</Tabs>
)
)}
{children}
</div>
</div>
<AddFieldModal
open={addFieldModalOpen}
onOpenChange={handleModalOpenChange}
sectionName={activeSectionTab}
builtinSection={builtinSection}
initialField={editingField}
onAdd={handleAddField}
/>
</>
);
}

View File

@@ -0,0 +1,43 @@
import { useTranslation } from 'react-i18next';
import { JsonPreviewSheet } from './json-preview-sheet';
import { WorkflowPreviewSheet } from './workflow-preview-sheet';
interface TemplatePreviewHeaderProps {
templateName: string | undefined;
jsonSheetOpen: boolean;
onJsonSheetOpenChange: (open: boolean) => void;
workflowSheetOpen: boolean;
onWorkflowSheetOpenChange: (open: boolean) => void;
allFormValues: Record<string, unknown>;
}
export function TemplatePreviewHeader({
templateName,
jsonSheetOpen,
onJsonSheetOpenChange,
workflowSheetOpen,
onWorkflowSheetOpenChange,
allFormValues,
}: TemplatePreviewHeaderProps) {
const { t } = useTranslation();
return (
<section className="shrink-0 flex justify-between items-center px-5 py-4 border-b border-border-button">
<span className="text-lg font-medium text-text-primary">
{templateName || t('setting.templateName')}
</span>
<div className="flex items-center gap-2">
<JsonPreviewSheet
open={jsonSheetOpen}
onOpenChange={onJsonSheetOpenChange}
value={allFormValues}
/>
<WorkflowPreviewSheet
open={workflowSheetOpen}
onOpenChange={onWorkflowSheetOpenChange}
/>
</div>
</section>
);
}

View File

@@ -0,0 +1,52 @@
import { Collapse } from '@/components/collapse';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { SwitchFormField } from '@/components/switch-fom-field';
import { Textarea } from '@/components/ui/textarea';
import { useTranslation } from 'react-i18next';
type TreeTemplateFieldsProps = {
index: number;
};
export function TreeTemplateFields({ index }: TreeTemplateFieldsProps) {
const { t } = useTranslation();
return (
<Collapse defaultOpen title={t('setting.raptorTreeSettings')}>
<div className="space-y-4">
<RAGFlowFormItem
name={`templates.${index}.config.raptor.prompt`}
label={t('setting.summarizationPrompt')}
>
<Textarea
placeholder={t('setting.descriptionPlaceholder')}
rows={6}
/>
</RAGFlowFormItem>
<SliderInputFormField
name={`templates.${index}.config.raptor.max_token`}
label={t('setting.maxToken')}
max={2048}
min={0}
step={1}
/>
<SliderInputFormField
name={`templates.${index}.config.raptor.threshold`}
label={t('setting.threshold')}
step={0.01}
max={1}
min={0}
/>
<SwitchFormField
name={`templates.${index}.config.raptor.rechunk`}
label={t('setting.rechunkByTreeLeaves')}
tooltip={t('setting.rechunkByTreeLeavesTip')}
vertical={false}
/>
</div>
</Collapse>
);
}

View File

@@ -0,0 +1,56 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Workflow } from 'lucide-react';
interface WorkflowPreviewSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function WorkflowPreviewSheet({
open,
onOpenChange,
}: WorkflowPreviewSheetProps) {
const { t } = useTranslation();
return (
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
<Tooltip>
<TooltipTrigger asChild>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<Workflow className="size-4" />
</Button>
</SheetTrigger>
</TooltipTrigger>
<TooltipContent>{t('setting.processFlow')}</TooltipContent>
</Tooltip>
<SheetContent
className="w-1/2 max-w-[700px] flex flex-col"
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader>
<SheetTitle>{t('setting.processFlow')}</SheetTitle>
</SheetHeader>
<div className="flex-1 min-h-0 mt-4 flex items-center justify-center">
<span className="text-text-disabled">
{t('setting.processFlowComingSoon')}
</span>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react';
export const useActiveSectionTab = (sectionNames: string[]) => {
const [activeSectionTab, setActiveSectionTab] = useState<string>('');
useEffect(() => {
setActiveSectionTab((prev) =>
sectionNames.includes(prev) ? prev : (sectionNames[0] ?? ''),
);
}, [sectionNames]);
return { activeSectionTab, setActiveSectionTab };
};

View File

@@ -0,0 +1,115 @@
import { ICompilationTemplateSection } from '@/interfaces/database/compilation-template';
import {
createEmptyField,
getFieldKeyOrder,
getTypeOptionsFromBuiltinSection,
} from '../utils';
import { useCallback, useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';
type UseAddFieldFormOptions = {
open: boolean;
builtinSection?: ICompilationTemplateSection;
initialField?: Record<string, string>;
};
export const useAddFieldForm = ({
open,
builtinSection,
initialField,
}: UseAddFieldFormOptions) => {
const form = useForm<Record<string, string>>({
defaultValues: {},
});
const fieldKeys = useMemo(() => {
const firstField = builtinSection?.fields?.[0];
const keys = firstField
? Object.keys(firstField)
: ['type', 'description', 'rule'];
return getFieldKeyOrder(keys);
}, [builtinSection]);
const hasTypeField = fieldKeys.includes('type');
const typeOptions = useMemo(
() => getTypeOptionsFromBuiltinSection(builtinSection),
[builtinSection],
);
const buildField = useCallback(
(typeValue: string) => {
const matched = builtinSection?.fields?.find(
(field) => field.type === typeValue,
);
if (matched) {
const normalized: Record<string, string> = {};
fieldKeys.forEach((key) => {
normalized[key] = (matched as Record<string, string>)[key] ?? '';
});
return normalized;
}
const empty = createEmptyField(fieldKeys);
if (hasTypeField) {
empty.type = typeValue;
}
return empty;
},
[builtinSection, fieldKeys, hasTypeField],
);
useEffect(() => {
if (!open) return;
if (initialField) {
const normalized: Record<string, string> = {};
fieldKeys.forEach((key) => {
normalized[key] = initialField[key] ?? '';
});
form.reset(normalized);
return;
}
const firstType = typeOptions[0]?.value ?? '';
form.reset(buildField(firstType));
}, [buildField, fieldKeys, form, initialField, open, typeOptions]);
const handleTypeChange = useCallback(
(value: string) => {
if (initialField) {
form.setValue('type', value, {
shouldDirty: true,
shouldTouch: true,
});
return;
}
const nextField = buildField(value);
Object.entries(nextField).forEach(([key, val]) => {
form.setValue(key, val, {
shouldDirty: true,
shouldTouch: true,
});
});
},
[buildField, form, initialField],
);
const handleSubmit = useCallback(
(onAdd: (field: Record<string, string>) => void) => {
return form.handleSubmit((values) => {
onAdd(values);
});
},
[form],
);
return {
form,
fieldKeys,
hasTypeField,
typeOptions,
handleTypeChange,
handleSubmit,
};
};

View File

@@ -0,0 +1,53 @@
import { CompilationTemplateKind } from '@/constants/compilation';
import { FormSchemaType } from '../schema';
import { useMemo } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
export const useAvailableKindOptions = (
form: UseFormReturn<FormSchemaType>,
kindOptions: { label: string; value: string }[],
selectedTemplateIndex: number,
) => {
const kind = useWatch({
control: form.control,
name: `templates.${selectedTemplateIndex}.kind`,
});
const templates = useWatch({ control: form.control, name: 'templates' });
const availableKindOptions = useMemo(() => {
const otherSelectedKinds = new Set(
templates
?.filter((_, index) => index !== selectedTemplateIndex)
.map((template) => template.kind)
.filter((value): value is string => Boolean(value)) ?? [],
);
const hasOtherNonArtifactsKind = Array.from(otherSelectedKinds).some(
(value) => value !== CompilationTemplateKind.Artifacts,
);
const hasOtherArtifactsKind = otherSelectedKinds.has(
CompilationTemplateKind.Artifacts,
);
return kindOptions.filter((option) => {
if (option.value === kind) return true;
if (otherSelectedKinds.has(option.value)) return false;
if (
hasOtherNonArtifactsKind &&
option.value === CompilationTemplateKind.Artifacts
) {
return false;
}
if (
hasOtherArtifactsKind &&
option.value !== CompilationTemplateKind.Artifacts
) {
return false;
}
return true;
});
}, [kindOptions, kind, selectedTemplateIndex, templates]);
return availableKindOptions;
};

View File

@@ -0,0 +1,132 @@
import { SelectWithSearchFlagOptionType } from '@/components/originui/select-with-search';
import { IWikiPreset } from '@/interfaces/database/compilation-template';
import { useCallback, useMemo, useState } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormSchemaType } from '../schema';
export const CustomBlueprintValue = '__custom__';
type UseBlueprintSelectionParams = {
form: UseFormReturn<FormSchemaType>;
selectedTemplateIndex: number;
presets: IWikiPreset[];
};
const isSameBlueprintContent = (
preset: IWikiPreset,
instruction: string,
pageExample: string,
) =>
preset.instruction.trim() === instruction.trim() &&
preset.page_example.trim() === pageExample.trim();
export function useBlueprintSelection({
form,
selectedTemplateIndex,
presets,
}: UseBlueprintSelectionParams) {
const { t } = useTranslation();
const [explicitValue, setExplicitValue] = useState<string>();
const instructionPath =
`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 instruction = useWatch({
control: form.control,
name: instructionPath,
});
const pageExample = useWatch({
control: form.control,
name: pageExamplePath,
});
const matchedPresetId = useMemo(() => {
const currentInstruction = String(instruction ?? '');
const currentPageExample = String(pageExample ?? '');
if (!currentInstruction.trim() && !currentPageExample.trim()) {
return undefined;
}
return presets.find((preset) =>
isSameBlueprintContent(preset, currentInstruction, currentPageExample),
)?.id;
}, [instruction, pageExample, presets]);
const selectedValue =
explicitValue ?? matchedPresetId ?? CustomBlueprintValue;
const options = useMemo<SelectWithSearchFlagOptionType[]>(
() => [
...presets.map((preset) => ({
label: preset.id,
value: preset.id,
})),
{ label: t('setting.custom'), value: CustomBlueprintValue },
],
[presets, t],
);
const handleSelect = useCallback(
(value: string) => {
if (value === selectedValue) return;
setExplicitValue(value);
if (value === CustomBlueprintValue) {
const defaultConfig =
form.formState.defaultValues?.templates?.[selectedTemplateIndex]
?.config;
form.setValue(
instructionPath,
String(defaultConfig?.instruction ?? ''),
{ shouldValidate: false },
);
form.setValue(
pageExamplePath,
String(defaultConfig?.page_example ?? ''),
{ shouldValidate: false },
);
} else {
const preset = presets.find((item) => item.id === value);
if (!preset) return;
form.setValue(instructionPath, preset.instruction, {
shouldValidate: false,
});
form.setValue(pageExamplePath, preset.page_example, {
shouldValidate: false,
});
}
form.setValue(useBlueprintPath, true, { shouldValidate: false });
},
[
form,
instructionPath,
pageExamplePath,
presets,
selectedTemplateIndex,
selectedValue,
useBlueprintPath,
],
);
const handlePageExampleChange = useCallback(
(value: string) => {
form.setValue(pageExamplePath, value, { shouldValidate: false });
},
[form, pageExamplePath],
);
return {
selectedValue,
options,
handleSelect,
instructionPath,
pageExample,
handlePageExampleChange,
};
}

View File

@@ -0,0 +1,32 @@
import { useMemo } from 'react';
import {
ICompilationTemplateBuiltin,
ICompilationTemplateSection,
} from '@/interfaces/database/compilation-template';
import { isConfigMetaKey, sortSectionNames } from '../utils';
export const useBuiltinTemplate = (
builtins: ICompilationTemplateBuiltin[],
kind: string,
) => {
const builtinTemplate = useMemo(
() => builtins.find((template) => template.kind === kind),
[builtins, kind],
);
const sectionNames = useMemo(() => {
const names = Object.keys(builtinTemplate?.config ?? {}).filter((key) => {
if (isConfigMetaKey(key)) return false;
const section = builtinTemplate?.config?.[key];
return (
section &&
typeof section === 'object' &&
Array.isArray((section as ICompilationTemplateSection).fields)
);
});
return sortSectionNames(names);
}, [builtinTemplate]);
return { builtinTemplate, sectionNames };
};

View File

@@ -0,0 +1,43 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template';
import { buildFormSchema, FormSchemaType } from '../schema';
import { DefaultValues, transformGroupDetailToForm } from '../utils';
type UseCompilationTemplateGroupFormOptions = {
detail?: ICompilationTemplateGroup;
defaultLlmId?: string;
isCreate: boolean;
};
export const useCompilationTemplateGroupForm = ({
detail,
defaultLlmId,
isCreate,
}: UseCompilationTemplateGroupFormOptions) => {
const { t } = useTranslation();
const form = useForm<FormSchemaType>({
resolver: zodResolver(buildFormSchema(t)),
defaultValues: DefaultValues,
mode: 'onChange',
});
useEffect(() => {
if (detail) {
form.reset(transformGroupDetailToForm(detail));
} else if (
isCreate &&
defaultLlmId &&
!form.getValues('templates.0.llm_id')
) {
form.setValue('templates.0.llm_id', defaultLlmId);
}
}, [defaultLlmId, detail, form, isCreate]);
return { form };
};

View File

@@ -0,0 +1,48 @@
import { useCallback } from 'react';
import {
ICreateCompilationTemplateGroupRequestBody,
IUpdateCompilationTemplateGroupRequestBody,
} from '@/interfaces/request/compilation-template';
import { FormSchemaType } from '../schema';
import { transformFormToPayload } from '../utils';
type UseCompilationTemplateGroupSubmitOptions = {
isCreate: boolean;
id?: string;
createGroup: (
params: ICreateCompilationTemplateGroupRequestBody,
) => Promise<{ code: number } & Record<string, unknown>>;
updateGroup: (
id: string,
params: IUpdateCompilationTemplateGroupRequestBody,
) => Promise<{ code: number } & Record<string, unknown>>;
onSuccess: () => void;
};
export const useCompilationTemplateGroupSubmit = ({
isCreate,
id,
createGroup,
updateGroup,
onSuccess,
}: UseCompilationTemplateGroupSubmitOptions) => {
const onSubmit = useCallback(
async (values: FormSchemaType) => {
const payload = transformFormToPayload(values);
let result;
if (isCreate) {
result = await createGroup(payload);
} else if (id) {
result = await updateGroup(id, payload);
}
if (result?.code === 0) {
onSuccess();
}
},
[createGroup, id, isCreate, onSuccess, updateGroup],
);
return { onSubmit };
};

View File

@@ -0,0 +1,75 @@
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
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 { isCreateCompilationTemplateGroup } from '@/utils/compilation-template-util';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { formatKindLabel } from '@/utils/compilation-template-util';
import { useCompilationTemplateGroupForm } from './use-compilation-template-group-form';
import { useCompilationTemplateGroupSubmit } from './use-compilation-template-group-submit';
type UseEditNextCompilationTemplateGroupOptions = {
onSuccess?: () => void;
};
export const useEditNextCompilationTemplateGroup = ({
onSuccess,
}: UseEditNextCompilationTemplateGroupOptions = {}) => {
const { id } = useParams<{ id: string }>();
const { navigateToCompilationTemplates } = useNavigatePage();
const { t } = useTranslation();
const isCreate = isCreateCompilationTemplateGroup(id);
const { data: detail } = useFetchCompilationTemplateGroup();
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: formatKindLabel(t, option.value),
})),
[builtinKindOptions, t],
);
const { form } = useCompilationTemplateGroupForm({
detail,
defaultLlmId: defaultModelDictionary.llm_id,
isCreate,
});
const { onSubmit } = useCompilationTemplateGroupSubmit({
isCreate,
id,
createGroup,
updateGroup,
onSuccess: onSuccess ?? navigateToCompilationTemplates,
});
return {
id,
form,
kindOptions,
builtins,
onSubmit,
isCreate,
isLoading: isCreate ? createLoading : updateLoading,
navigateToCompilationTemplates,
};
};

View File

@@ -0,0 +1,39 @@
import { useCallback } from 'react';
import { ArrayPath, UseFormReturn } from 'react-hook-form';
import { FormSchemaType } from '../schema';
export const useFieldArrayHandlers = (
form: UseFormReturn<FormSchemaType>,
activeFieldsPath: ArrayPath<FormSchemaType>,
editingFieldIndex: number | null,
setEditingFieldIndex: (index: number | null) => void,
) => {
const handleAddField = useCallback(
(field: Record<string, string>) => {
const currentFields =
(form.getValues(activeFieldsPath) as
| Record<string, string>[]
| undefined) ?? [];
if (editingFieldIndex !== null) {
const newFields = [...currentFields];
newFields[editingFieldIndex] = field;
form.setValue(activeFieldsPath, newFields, {
shouldValidate: false,
shouldDirty: true,
shouldTouch: true,
});
} else {
form.setValue(activeFieldsPath, [...currentFields, field], {
shouldValidate: false,
shouldDirty: true,
shouldTouch: true,
});
}
setEditingFieldIndex(null);
},
[activeFieldsPath, editingFieldIndex, form, setEditingFieldIndex],
);
return { handleAddField };
};

View File

@@ -0,0 +1,35 @@
import { useCallback, useState } from 'react';
export const useFieldModal = () => {
const [addFieldModalOpen, setAddFieldModalOpen] = useState(false);
const [editingFieldIndex, setEditingFieldIndex] = useState<number | null>(
null,
);
const handleModalOpenChange = useCallback(
(open: boolean) => {
setAddFieldModalOpen(open);
if (!open) setEditingFieldIndex(null);
},
[setAddFieldModalOpen],
);
const handleOpenAddField = useCallback(() => {
setEditingFieldIndex(null);
setAddFieldModalOpen(true);
}, []);
const handleOpenEditField = useCallback((index: number) => {
setEditingFieldIndex(index);
setAddFieldModalOpen(true);
}, []);
return {
addFieldModalOpen,
editingFieldIndex,
setEditingFieldIndex,
handleModalOpenChange,
handleOpenAddField,
handleOpenEditField,
};
};

View File

@@ -0,0 +1,42 @@
import { ICompilationTemplateBuiltin } from '@/interfaces/database/compilation-template';
import { UseFormReturn } from 'react-hook-form';
import { FormSchemaType } from '../schema';
import { buildConfigFromBuiltin } from '../utils';
type FieldLike = {
value: string;
onChange: (value: string) => void;
};
type UseTemplateKindChangeOptions = {
form: UseFormReturn<FormSchemaType>;
index: number;
builtins: ICompilationTemplateBuiltin[];
};
export const useTemplateKindChange = ({
form,
index,
builtins,
}: UseTemplateKindChangeOptions) => {
return (field: FieldLike, value: string) => {
if (value && value !== field.value) {
const builtinTemplate = builtins.find(
(template) => template.kind === value,
);
if (builtinTemplate) {
form.setValue(
`templates.${index}.config`,
buildConfigFromBuiltin(
builtinTemplate,
value,
form.getValues(`templates.${index}.llm_id`),
),
{ shouldValidate: false },
);
}
}
field.onChange(value);
};
};

View File

@@ -0,0 +1,28 @@
import { useState } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { FormSchemaType } from '../schema';
export function useTemplatePreviewSheets(
form: UseFormReturn<FormSchemaType>,
selectedTemplateIndex: number,
) {
const [jsonSheetOpen, setJsonSheetOpen] = useState(false);
const [workflowSheetOpen, setWorkflowSheetOpen] = useState(false);
const allFormValues = useWatch({ control: form.control });
const templateName = useWatch({
control: form.control,
name: `templates.${selectedTemplateIndex}.name`,
});
return {
jsonSheetOpen,
setJsonSheetOpen,
workflowSheetOpen,
setWorkflowSheetOpen,
allFormValues,
templateName,
};
}

View File

@@ -0,0 +1,40 @@
import { useMemo } from 'react';
import { ArrayPath, UseFormReturn } from 'react-hook-form';
import {
ICompilationTemplateBuiltin,
ICompilationTemplateSection,
} from '@/interfaces/database/compilation-template';
import { FormSchemaType } from '../schema';
export const useTemplateSectionData = (
form: UseFormReturn<FormSchemaType>,
selectedTemplateIndex: number,
activeSectionTab: string,
builtinTemplate: ICompilationTemplateBuiltin | undefined,
editingFieldIndex: number | null,
) => {
const activeSectionPath = `templates.${selectedTemplateIndex}.config.${activeSectionTab}`;
const activeFieldsPath =
`${activeSectionPath}.fields` as ArrayPath<FormSchemaType>;
const builtinSection = useMemo(() => {
return builtinTemplate?.config?.[activeSectionTab] as
| ICompilationTemplateSection
| undefined;
}, [activeSectionTab, builtinTemplate?.config]);
const editingField = useMemo(() => {
if (editingFieldIndex === null) return undefined;
return ((form.getValues(activeFieldsPath) as
| Record<string, string>[]
| undefined) ?? [])[editingFieldIndex];
}, [activeFieldsPath, editingFieldIndex, form]);
return {
activeSectionPath,
activeFieldsPath,
builtinSection,
editingField,
};
};

View File

@@ -0,0 +1,85 @@
import BackButton from '@/components/back-button';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { CompilationTemplateKind } from '@/constants/compilation';
import { useFetchCompilationTemplateGroup } from '@/hooks/use-compilation-template-group-request';
import { Routes } from '@/routes';
import { useCallback, useMemo } from 'react';
import { useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { BlueprintSection } from './components/blueprint-section';
import { TemplateConfiguration } from './components/template-configuration';
import { useEditNextCompilationTemplateGroup } from './hooks/use-edit-next-compilation-template-group';
const SelectedTemplateIndex = 0;
const agentsUrl = Routes.Agents;
export default function EditNextCompilationTemplate() {
const { t } = useTranslation();
const navigate = useNavigate();
const navigateToAgents = useCallback(() => {
navigate(agentsUrl);
}, [navigate]);
const { form, kindOptions, builtins, onSubmit, isCreate, isLoading } =
useEditNextCompilationTemplateGroup({
onSuccess: navigateToAgents,
});
const { data: group } = useFetchCompilationTemplateGroup();
const selectedKind = useWatch({
control: form.control,
name: `templates.${SelectedTemplateIndex}.kind`,
});
const isArtifacts = selectedKind === CompilationTemplateKind.Artifacts;
const handleSave = useMemo(
() => form.handleSubmit(onSubmit),
[form, onSubmit],
);
return (
<section className="h-full flex flex-col bg-bg-base">
<header className="shrink-0 px-5 py-4 border-b border-border-button flex gap-3 items-center">
<BackButton to={agentsUrl} />
<h2 className="font-medium text-text-secondary">
{isCreate
? t('setting.addTemplateGroup')
: group?.name || t('setting.editTemplateGroup')}
</h2>
</header>
<Form {...form}>
<form className="flex-1 min-h-0 flex flex-col">
<TemplateConfiguration
form={form}
builtins={builtins}
kindOptions={kindOptions}
selectedTemplateIndex={SelectedTemplateIndex}
>
{isArtifacts && (
<BlueprintSection
form={form}
selectedTemplateIndex={SelectedTemplateIndex}
/>
)}
</TemplateConfiguration>
<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={navigateToAgents}>
{t('common.back')}
</Button>
<Button type="button" loading={isLoading} onClick={handleSave}>
{t('common.save')}
</Button>
</footer>
</form>
</Form>
</section>
);
}

View File

@@ -0,0 +1,57 @@
import { z } from 'zod';
export const buildSectionSchema = (t: (key: string) => string) =>
z.object({
description: z.string().optional(),
fields: z
.array(z.record(z.string().min(1, t('setting.fieldDescriptionRequired'))))
.min(1),
});
export const buildRaptorConfigSchema = (t: (key: string) => string) =>
z.object({
prompt: z.string().optional(),
max_token: z.number().min(1, t('setting.maxTokenRequired')),
threshold: z.number().min(0).max(1),
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(),
name: z.string().min(1, t('setting.templateNameRequired')),
description: z.string().optional(),
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),
buildSynthesisSchema(),
z.string(),
z.boolean(),
]),
),
});
export const buildFormSchema = (t: (key: string) => string) =>
z.object({
name: z.string().optional(),
description: z.string().optional(),
avatar: z.string().optional(),
templates: z.array(buildTemplateSchema(t)).min(1),
});
export type TemplateSchemaType = z.infer<
ReturnType<typeof buildTemplateSchema>
>;
export type FormSchemaType = z.infer<ReturnType<typeof buildFormSchema>>;

View File

@@ -0,0 +1,325 @@
import { isEqual } from 'lodash';
import {
ICompilationTemplate,
ICompilationTemplateBuiltin,
ICompilationTemplateGroup,
ICompilationTemplateRaptorConfig,
ICompilationTemplateSection,
} from '@/interfaces/database/compilation-template';
import { ICompilationTemplateConfigRequest } from '@/interfaces/request/compilation-template';
import { CompilationTemplateKind } from '@/constants/compilation';
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'],
['definition_excerpt', 'term'],
];
export const getFieldKeyOrder = (keys: string[]): string[] => {
const sortedKeys = [...keys].sort();
return (
FieldKeyOrders.find((order) => isEqual([...order].sort(), sortedKeys)) ??
keys
);
};
export const DefaultTemplateValues: TemplateSchemaType = {
id: undefined,
name: '',
description: '',
llm_id: '',
kind: '',
config: {
kind: '',
llm_id: '',
global_rules: '',
example: '',
instruction: '',
page_example: '',
use_blueprint: false,
},
};
export const DefaultValues: FormSchemaType = {
name: '',
description: '',
avatar: '',
templates: [DefaultTemplateValues],
};
export const isConfigMetaKey = (key: string) =>
[
'kind',
'llm_id',
'global_rules',
'example',
'instruction',
'page_example',
'synthesis',
'use_blueprint',
].includes(key);
export const createEmptyField = (keys: string[]) =>
Object.fromEntries(keys.map((key) => [key, '']));
export const normalizeSection = (
section?: ICompilationTemplateSection,
): ICompilationTemplateSection => {
const fields = section?.fields ?? [];
return {
description: section?.description ?? '',
fields:
fields.length > 0
? fields.map((field) =>
Object.fromEntries(
Object.entries(field).map(([key, value]) => [key, value ?? '']),
),
)
: [createEmptyField(DefaultFieldKeys)],
};
};
export const buildConfigFromBuiltin = (
builtinTemplate: ICompilationTemplateBuiltin,
kind: string,
llmId: string,
): TemplateSchemaType['config'] => {
const example =
typeof builtinTemplate.config?.example === 'string'
? builtinTemplate.config.example
: '';
const sections: TemplateSchemaType['config'] = {
kind,
llm_id: llmId,
global_rules:
typeof builtinTemplate.config?.global_rules === 'string'
? builtinTemplate.config.global_rules
: '',
example:
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 ?? {};
return {
...sections,
raptor: {
prompt: builtinRaptor.prompt ?? '',
max_token: builtinRaptor.max_token ?? 512,
threshold: builtinRaptor.threshold ?? 0.1,
rechunk: builtinRaptor.rechunk ?? false,
},
};
}
Object.entries(builtinTemplate.config ?? {}).forEach(([key, value]) => {
if (isConfigMetaKey(key)) return;
sections[key] = normalizeSection(
value as ICompilationTemplateSection,
) as TemplateSchemaType['config'][string];
});
return sections;
};
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 {
id: detail.id,
name: detail.name ?? '',
description: detail.description ?? '',
llm_id: config.llm_id ?? '',
kind: detail.kind ?? '',
config: {
...base,
raptor: {
prompt: raptor.prompt ?? '',
max_token: raptor.max_token ?? 512,
threshold: raptor.threshold ?? 0.1,
rechunk: raptor.rechunk ?? false,
},
},
};
}
Object.entries(config).forEach(([key, value]) => {
if (isConfigMetaKey(key)) return;
base[key] = normalizeSection(
value as ICompilationTemplateSection,
) as TemplateSchemaType['config'][string];
});
return {
id: detail.id,
name: detail.name ?? '',
description: detail.description ?? '',
llm_id: config.llm_id ?? '',
kind: detail.kind ?? '',
config: base,
};
};
export const transformGroupDetailToForm = (
detail: ICompilationTemplateGroup,
): FormSchemaType => {
const templates = (detail.templates ?? []).map((template) =>
transformDetailToForm(template),
);
return {
name: detail.name ?? '',
description: detail.description ?? '',
avatar: detail.avatar ?? '',
templates: templates.length > 0 ? templates : [DefaultTemplateValues],
};
};
export const transformTemplateToPayload = (template: TemplateSchemaType) => {
const config: ICompilationTemplateConfigRequest = {
kind: template.kind,
llm_id: template.llm_id,
};
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 {
config[key] = value as ICompilationTemplateConfigRequest[string];
}
});
if (template.kind === CompilationTemplateKind.Artifacts) {
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 {
id: template.id,
name: template.name,
description: template.description,
kind: template.kind,
config,
};
};
export const transformFormToPayload = (values: FormSchemaType) => {
const template = values.templates[0];
return {
name: template?.name ?? values.name ?? '',
description: template?.description ?? values.description,
avatar: values.avatar || undefined,
templates: values.templates.map((template) =>
transformTemplateToPayload(template),
),
};
};
export const SectionTitleKeyMap: Record<string, string> = {
entity: 'setting.entitySpecification',
relation: 'setting.relationSpecification',
concept: 'setting.conceptSpecification',
claim: 'setting.claimSpecification',
};
export const SectionPriority = ['entity', 'relation'];
export const sortSectionNames = (names: string[]): string[] => {
const priority = SectionPriority.filter((name) => names.includes(name));
const rest = names.filter((name) => !SectionPriority.includes(name));
return [...priority, ...rest];
};
export const FieldLabelKeyMap: Record<string, string> = {
type: 'setting.fieldType',
description: 'setting.fieldDescription',
rule: 'setting.fieldRule',
};
export const getTypeOptionsFromBuiltinSection = (
builtinSection?: ICompilationTemplateSection,
) => {
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 }));
};

View File

@@ -27,7 +27,10 @@ export default function CompilationTemplates() {
} = useFetchCompilationTemplateGroupsByPage();
const { deleteGroup } = useDeleteCompilationTemplateGroup();
const { navigateToCompilationTemplate } = useNavigatePage();
const {
navigateToCompilationTemplate,
navigateToCompilationTemplateEditNext,
} = useNavigatePage();
const handlePageChange = useCallback(
(page: number, pageSize?: number) => {
@@ -40,6 +43,10 @@ export default function CompilationTemplates() {
navigateToCompilationTemplate('create')();
}, [navigateToCompilationTemplate]);
const handleAddNext = useCallback(() => {
navigateToCompilationTemplateEditNext()();
}, [navigateToCompilationTemplateEditNext]);
const handleDelete = useCallback(
async (id: string) => {
await deleteGroup(id);
@@ -73,6 +80,10 @@ export default function CompilationTemplates() {
<Plus />
{t('setting.addTemplateGroup')}
</Button>
<Button onClick={handleAddNext}>
<Plus />
Next
</Button>
</div>
</header>
}
@@ -85,7 +96,7 @@ export default function CompilationTemplates() {
<TemplateCard
key={item.id}
data={item}
onClick={navigateToCompilationTemplate(item.id)}
onClick={navigateToCompilationTemplateEditNext(item.id)}
onDelete={handleDelete}
/>
))}

View File

@@ -6,6 +6,7 @@ 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';
@@ -28,6 +29,7 @@ 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],
@@ -63,7 +65,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">
{formatKindLabel(kind)}
{formatKindLabel(t, kind)}
</Badge>
))}
</div>

View File

@@ -12,7 +12,6 @@ import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { TFunction } from 'i18next';
import {
Columns3Cog,
LucideBox,
LucideLogOut,
LucideMessagesSquare,
@@ -47,11 +46,6 @@ const menuItems = (t: TFunction) => [
label: 'MCP',
key: Routes.Mcp,
},
{
icon: <Columns3Cog className="size-[1em]" />,
label: t('setting.compilationTemplates'),
key: Routes.CompilationTemplates,
},
{
icon: <LucideUsers className="size-[1em]" />,
label: t('setting.team'),