diff --git a/web/AGENTS.md b/web/AGENTS.md index f88509918d..54be7647b1 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -125,6 +125,14 @@ Interface types are split between two folders: Model-related endpoints (LLM provider / factory / my LLM, etc.) are consolidated in `src/services/llm-service.ts` rather than scattered across hooks. For GET endpoints, register with `method: 'get'` in the service, and on the call site pass `true` as the second argument to use the native axios config (e.g., `service.listProviders({ params: { available: true } }, true)`). +### Module Dependency Direction + +`src/components/`, `src/hooks/`, `src/services/`, and `src/utils/` are shared layers: **they must not import from `src/pages/`**. A page may import from shared layers, never the reverse. (Legacy exceptions exist, e.g. `src/components/pipeline-operator-tabs/`; do not add new ones, and prefer lifting when you touch such code.) + +When page code gains a second consumer (e.g. a page form reused inside a dialog), lift the **whole self-contained subtree** — its `components/`, `hooks/`, `schema.ts`, `constant.ts`, `utils.ts` — into a feature folder under `src/components/` (precedent: `compilation-template-group/`, `pipeline-operator-tabs/`) using `git mv` to preserve history. Keep the subtree's internal structure and relative imports unchanged, and slim the route page down to a thin wrapper (route params, navigation, header). Never lift a single file out of a subtree — that multiplies cross-layer imports instead of removing them. Page-only logic (route params, return-navigation helpers) stays in the page directory. + +`src/utils/` is for pure functions only — shared React components and hooks live in `src/components/` and `src/hooks/`, never in `src/utils/`. + ### Shared UI Component Lock The folder `src/components/ui/` is the project's **shared UI library** — it contains both official shadcn/ui primitives and project-authored common components built on top of shadcn. Both kinds are intended to be reused across the app and **must not be modified casually**. diff --git a/web/src/components/compilation-template-form-field.test.tsx b/web/src/components/compilation-template-form-field.test.tsx index aadad00f9b..75db370fa2 100644 --- a/web/src/components/compilation-template-form-field.test.tsx +++ b/web/src/components/compilation-template-form-field.test.tsx @@ -16,7 +16,7 @@ import { Form } from '@/components/ui/form'; import { TooltipProvider } from '@/components/ui/tooltip'; -import { render, screen, within } from '@testing-library/react'; +import { act, fireEvent, render, screen, within } from '@testing-library/react'; import { useForm } from 'react-hook-form'; import { CompilationTemplateFormField } from './compilation-template-form-field'; @@ -32,6 +32,7 @@ beforeAll(() => { }); const mockUseCompilationTemplateGroupOptions = jest.fn(); +const mockCreateDialog = jest.fn(() => null); jest.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -44,10 +45,21 @@ jest.mock('@/hooks/use-compilation-template-group-request', () => ({ mockUseCompilationTemplateGroupOptions(...args), })); -jest.mock('@/hooks/logic-hooks/navigate-hooks', () => ({ - useNavigatePage: () => ({ navigateToAgents: jest.fn() }), +jest.mock('./create-compilation-template-group-dialog', () => ({ + CreateCompilationTemplateGroupDialog: (props: unknown) => + mockCreateDialog(props), })); +type DialogProps = { + open: boolean; + onCreated: (group: { id: string }) => void; +}; + +const lastDialogProps = (): DialogProps => + mockCreateDialog.mock.calls.at(-1)?.[0] as DialogProps; + +let capturedForm: { getValues: (name: string) => unknown }; + function Harness({ groupId, ownerTenantId, @@ -58,6 +70,7 @@ function Harness({ const form = useForm({ defaultValues: { compilation_template_group_id: groupId }, }); + capturedForm = form; return ( @@ -72,13 +85,16 @@ function Harness({ } describe('CompilationTemplateFormField', () => { - it('shows a warning marker for a group the current user cannot resolve', () => { + beforeEach(() => { + mockCreateDialog.mockClear(); mockUseCompilationTemplateGroupOptions.mockReturnValue({ options: [{ label: 'Group A', value: 'g1' }], isFetched: true, isError: false, }); + }); + it('shows a warning marker for a group the current user cannot resolve', () => { render(); const trigger = screen.getByRole('combobox'); @@ -101,12 +117,6 @@ describe('CompilationTemplateFormField', () => { }); it('shows the option label for a resolvable group', () => { - mockUseCompilationTemplateGroupOptions.mockReturnValue({ - options: [{ label: 'Group A', value: 'g1' }], - isFetched: true, - isError: false, - }); - render(); const trigger = screen.getByRole('combobox'); @@ -115,16 +125,40 @@ describe('CompilationTemplateFormField', () => { }); it('requests the options of the canvas owner tenant', () => { - mockUseCompilationTemplateGroupOptions.mockReturnValue({ - options: [], - isFetched: true, - isError: false, - }); - render(); expect(mockUseCompilationTemplateGroupOptions).toHaveBeenCalledWith( 'owner-1', ); }); + + it('opens the create dialog from the label link', () => { + render(); + + fireEvent.click(screen.getByText('knowledgeConfiguration.createTemplate')); + + expect(lastDialogProps().open).toBe(true); + }); + + it('adopts the created group when nothing is selected', async () => { + render(); + + await act(async () => { + lastDialogProps().onCreated({ id: 'new-group' }); + }); + + expect(capturedForm.getValues('compilation_template_group_id')).toBe( + 'new-group', + ); + }); + + it('keeps the current selection when one already exists', async () => { + render(); + + await act(async () => { + lastDialogProps().onCreated({ id: 'new-group' }); + }); + + expect(capturedForm.getValues('compilation_template_group_id')).toBe('g1'); + }); }); diff --git a/web/src/components/compilation-template-form-field.tsx b/web/src/components/compilation-template-form-field.tsx index 7633d1d7a9..e2c3aadbbe 100644 --- a/web/src/components/compilation-template-form-field.tsx +++ b/web/src/components/compilation-template-form-field.tsx @@ -16,10 +16,12 @@ import { RAGFlowFormItem } from '@/components/ragflow-form'; import { useCompilationTemplateGroupOptions } from '@/hooks/use-compilation-template-group-request'; -import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template'; import { TriangleAlert } from 'lucide-react'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { CreateCompilationTemplateGroupDialog } from './create-compilation-template-group-dialog'; import { SelectWithSearch } from './originui/select-with-search'; type CompilationTemplateFormFieldProps = { @@ -34,15 +36,28 @@ export function CompilationTemplateFormField({ ownerTenantId, }: CompilationTemplateFormFieldProps) { const { t } = useTranslation(); - const { navigateToAgents } = useNavigatePage(); + const { getValues, setValue } = useFormContext(); const { options, isFetched, isError } = useCompilationTemplateGroupOptions(ownerTenantId); + const [createDialogOpen, setCreateDialogOpen] = useState(false); - // Zero-arg wrapper: labelLink.onClick is wired to a DOM click, which would - // feed the event into navigateToAgents' canvasCategory parameter. - const handleCreateTemplateClick = useCallback(() => { - navigateToAgents(); - }, [navigateToAgents]); + // Zero-arg wrapper: labelLink.onClick is wired to a DOM click, whose event + // would be read as the updater value if setCreateDialogOpen were passed + // directly. + const openCreateDialog = useCallback(() => { + setCreateDialogOpen(true); + }, []); + + // Adopt the freshly created group only when the field is still empty — an + // existing selection is the user's and must survive template creation. + const adoptCreatedGroup = useCallback( + (group: ICompilationTemplateGroup) => { + if (!getValues(name)) { + setValue(name, group.id, { shouldDirty: true, shouldValidate: true }); + } + }, + [getValues, name, setValue], + ); // No owner-scoped name lookup exists for template groups (strictly // owner-tenant, no sharing mechanism) — the raw id is all the missing @@ -58,26 +73,33 @@ export function CompilationTemplateFormField({ ); return ( - - {(field) => ( - - )} - + <> + + {(field) => ( + + )} + + + ); } diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/add-field-modal.tsx b/web/src/components/compilation-template-group/components/add-field-modal.tsx similarity index 98% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/add-field-modal.tsx rename to web/src/components/compilation-template-group/components/add-field-modal.tsx index fbacc949d9..12324ff66c 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/components/add-field-modal.tsx +++ b/web/src/components/compilation-template-group/components/add-field-modal.tsx @@ -141,7 +141,8 @@ export function AddFieldModal({ rules={{ required: getRequiredMessage('type'), validate: (value: string) => - !isDuplicateType(value) || t('knowledgeCompilation.fieldTypeExists'), + !isDuplicateType(value) || + t('knowledgeCompilation.fieldTypeExists'), }} > {(field) => ( diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/blueprint-section.tsx b/web/src/components/compilation-template-group/components/blueprint-section.tsx similarity index 96% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/blueprint-section.tsx rename to web/src/components/compilation-template-group/components/blueprint-section.tsx index fde84bf1c3..17cbc30477 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/components/blueprint-section.tsx +++ b/web/src/components/compilation-template-group/components/blueprint-section.tsx @@ -52,7 +52,9 @@ export function BlueprintSection({ {t('knowledgeCompilation.blueprints')} +

+ {t('knowledgeCompilation.blueprints')} +

} >
diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/field-card.tsx b/web/src/components/compilation-template-group/components/field-card.tsx similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/field-card.tsx rename to web/src/components/compilation-template-group/components/field-card.tsx diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/json-preview-sheet.tsx b/web/src/components/compilation-template-group/components/json-preview-sheet.tsx similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/json-preview-sheet.tsx rename to web/src/components/compilation-template-group/components/json-preview-sheet.tsx diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/section-field-grid.tsx b/web/src/components/compilation-template-group/components/section-field-grid.tsx similarity index 96% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/section-field-grid.tsx rename to web/src/components/compilation-template-group/components/section-field-grid.tsx index 663c38ec7a..5439bc33d2 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/components/section-field-grid.tsx +++ b/web/src/components/compilation-template-group/components/section-field-grid.tsx @@ -84,7 +84,9 @@ export function SectionFieldGrid({ > - {t('knowledgeCompilation.addField')} + + {t('knowledgeCompilation.addField')} + diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/template-configuration.tsx b/web/src/components/compilation-template-group/components/template-configuration.tsx similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/template-configuration.tsx rename to web/src/components/compilation-template-group/components/template-configuration.tsx diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/template-preview-header.tsx b/web/src/components/compilation-template-group/components/template-preview-header.tsx similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/template-preview-header.tsx rename to web/src/components/compilation-template-group/components/template-preview-header.tsx diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/tree-template-fields.tsx b/web/src/components/compilation-template-group/components/tree-template-fields.tsx similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/tree-template-fields.tsx rename to web/src/components/compilation-template-group/components/tree-template-fields.tsx diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/workflow-preview-sheet.tsx b/web/src/components/compilation-template-group/components/workflow-preview-sheet.tsx similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/components/workflow-preview-sheet.tsx rename to web/src/components/compilation-template-group/components/workflow-preview-sheet.tsx diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/constant.ts b/web/src/components/compilation-template-group/constant.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/constant.ts rename to web/src/components/compilation-template-group/constant.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-active-section-tab.ts b/web/src/components/compilation-template-group/hooks/use-active-section-tab.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-active-section-tab.ts rename to web/src/components/compilation-template-group/hooks/use-active-section-tab.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-add-field-form.ts b/web/src/components/compilation-template-group/hooks/use-add-field-form.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-add-field-form.ts rename to web/src/components/compilation-template-group/hooks/use-add-field-form.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-available-kind-options.ts b/web/src/components/compilation-template-group/hooks/use-available-kind-options.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-available-kind-options.ts rename to web/src/components/compilation-template-group/hooks/use-available-kind-options.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-blueprint-selection.ts b/web/src/components/compilation-template-group/hooks/use-blueprint-selection.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-blueprint-selection.ts rename to web/src/components/compilation-template-group/hooks/use-blueprint-selection.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-builtin-template.ts b/web/src/components/compilation-template-group/hooks/use-builtin-template.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-builtin-template.ts rename to web/src/components/compilation-template-group/hooks/use-builtin-template.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-compilation-template-group-form.ts b/web/src/components/compilation-template-group/hooks/use-compilation-template-group-form.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-compilation-template-group-form.ts rename to web/src/components/compilation-template-group/hooks/use-compilation-template-group-form.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-compilation-template-group-submit.ts b/web/src/components/compilation-template-group/hooks/use-compilation-template-group-submit.ts similarity index 81% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-compilation-template-group-submit.ts rename to web/src/components/compilation-template-group/hooks/use-compilation-template-group-submit.ts index 7c055b6cd3..a71694ec53 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-compilation-template-group-submit.ts +++ b/web/src/components/compilation-template-group/hooks/use-compilation-template-group-submit.ts @@ -16,6 +16,7 @@ import { useCallback } from 'react'; +import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template'; import { ICreateCompilationTemplateGroupRequestBody, IUpdateCompilationTemplateGroupRequestBody, @@ -24,17 +25,22 @@ import { import { FormSchemaType } from '../schema'; import { transformFormToPayload } from '../utils'; +export type CompilationTemplateGroupSaveResult = { + code: number; + data?: ICompilationTemplateGroup; +}; + type UseCompilationTemplateGroupSubmitOptions = { isCreate: boolean; id?: string; createGroup: ( params: ICreateCompilationTemplateGroupRequestBody, - ) => Promise<{ code: number } & Record>; + ) => Promise; updateGroup: ( id: string, params: IUpdateCompilationTemplateGroupRequestBody, - ) => Promise<{ code: number } & Record>; - onSuccess: () => void; + ) => Promise; + onSuccess: (result: CompilationTemplateGroupSaveResult) => void; }; export const useCompilationTemplateGroupSubmit = ({ @@ -54,7 +60,7 @@ export const useCompilationTemplateGroupSubmit = ({ result = await updateGroup(id, payload); } if (result?.code === 0) { - onSuccess(); + onSuccess(result); } }, [createGroup, id, isCreate, onSuccess, updateGroup], diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-edit-next-compilation-template-group.ts b/web/src/components/compilation-template-group/hooks/use-edit-next-compilation-template-group.ts similarity index 83% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-edit-next-compilation-template-group.ts rename to web/src/components/compilation-template-group/hooks/use-edit-next-compilation-template-group.ts index 3033d1ff82..69b8033898 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-edit-next-compilation-template-group.ts +++ b/web/src/components/compilation-template-group/hooks/use-edit-next-compilation-template-group.ts @@ -23,26 +23,32 @@ import { useFetchBuiltinCompilationTemplates } from '@/hooks/use-compilation-tem 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'; +import { + CompilationTemplateGroupSaveResult, + useCompilationTemplateGroupSubmit, +} from './use-compilation-template-group-submit'; type UseEditNextCompilationTemplateGroupOptions = { - onSuccess: () => void; + // Explicit group id: the route page passes its :id param, embedded usages + // (create dialog) pass undefined — useParams would resolve to the host + // page's route params there. + id?: string; + onSuccess: (result: CompilationTemplateGroupSaveResult) => void; }; export const useEditNextCompilationTemplateGroup = ({ + id, onSuccess, }: UseEditNextCompilationTemplateGroupOptions) => { - const { id } = useParams<{ id: string }>(); const { t } = useTranslation(); const isCreate = isCreateCompilationTemplateGroup(id); - const { data: detail } = useFetchCompilationTemplateGroup(); + const { data: detail } = useFetchCompilationTemplateGroup(id); const { data: builtins, kindOptions: builtinKindOptions } = useFetchBuiltinCompilationTemplates(); @@ -71,12 +77,10 @@ export const useEditNextCompilationTemplateGroup = ({ }); return { - id, form, kindOptions, builtins, onSubmit, - isCreate, isLoading: isCreate ? createLoading : updateLoading, }; }; diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-field-array-handlers.ts b/web/src/components/compilation-template-group/hooks/use-field-array-handlers.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-field-array-handlers.ts rename to web/src/components/compilation-template-group/hooks/use-field-array-handlers.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-field-modal.ts b/web/src/components/compilation-template-group/hooks/use-field-modal.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-field-modal.ts rename to web/src/components/compilation-template-group/hooks/use-field-modal.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-template-kind-change.ts b/web/src/components/compilation-template-group/hooks/use-template-kind-change.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-template-kind-change.ts rename to web/src/components/compilation-template-group/hooks/use-template-kind-change.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-template-preview-sheets.ts b/web/src/components/compilation-template-group/hooks/use-template-preview-sheets.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-template-preview-sheets.ts rename to web/src/components/compilation-template-group/hooks/use-template-preview-sheets.ts diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-template-section-data.ts b/web/src/components/compilation-template-group/hooks/use-template-section-data.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/hooks/use-template-section-data.ts rename to web/src/components/compilation-template-group/hooks/use-template-section-data.ts diff --git a/web/src/components/compilation-template-group/index.tsx b/web/src/components/compilation-template-group/index.tsx new file mode 100644 index 0000000000..8918754ec6 --- /dev/null +++ b/web/src/components/compilation-template-group/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2026 The InfiniFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { CompilationTemplateKind } from '@/constants/compilation'; +import { useMemo } from 'react'; +import { useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { useEditNextCompilationTemplateGroup } from './hooks/use-edit-next-compilation-template-group'; +import { CompilationTemplateGroupSaveResult } from './hooks/use-compilation-template-group-submit'; +import { BlueprintSection } from './components/blueprint-section'; +import { TemplateConfiguration } from './components/template-configuration'; + +const SelectedTemplateIndex = 0; + +type CompilationTemplateGroupEditorProps = { + id?: string; + onSuccess: (result: CompilationTemplateGroupSaveResult) => void; + onCancel: () => void; + cancelText: string; +}; + +// Shared body of the edit-next route page and the create dialog: the form, +// its scrollable configuration area, and the cancel/save footer. The parent +// must give it a bounded-height flex column to fill. +export function CompilationTemplateGroupEditor({ + id, + onSuccess, + onCancel, + cancelText, +}: CompilationTemplateGroupEditorProps) { + const { t } = useTranslation(); + const { form, kindOptions, builtins, onSubmit, isLoading } = + useEditNextCompilationTemplateGroup({ id, onSuccess }); + + const selectedKind = useWatch({ + control: form.control, + name: `templates.${SelectedTemplateIndex}.kind`, + }); + + const isArtifacts = selectedKind === CompilationTemplateKind.Artifacts; + + const handleSave = useMemo( + () => form.handleSubmit(onSubmit), + [form, onSubmit], + ); + + return ( +
+ + + {isArtifacts && ( + + )} + + +
+ + +
+
+ + ); +} diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/schema.ts b/web/src/components/compilation-template-group/schema.ts similarity index 100% rename from web/src/pages/user-setting/compilation-templates/edit-next/schema.ts rename to web/src/components/compilation-template-group/schema.ts diff --git a/web/src/components/compilation-template-group/utils.ts b/web/src/components/compilation-template-group/utils.ts new file mode 100644 index 0000000000..f5802c4322 --- /dev/null +++ b/web/src/components/compilation-template-group/utils.ts @@ -0,0 +1,341 @@ +/* + * Copyright 2026 The InfiniFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isEqual } from 'lodash'; + +import { CompilationTemplateKind } from '@/constants/compilation'; +import { + ICompilationTemplate, + ICompilationTemplateBuiltin, + ICompilationTemplateGroup, + ICompilationTemplateRaptorConfig, + ICompilationTemplateSection, +} from '@/interfaces/database/compilation-template'; +import { ICompilationTemplateConfigRequest } from '@/interfaces/request/compilation-template'; + +import { FormSchemaType, TemplateSchemaType } from './schema'; +import { + DefaultTemplateValues, + FieldKeyOrders, + SectionPriority, +} from './constant'; + +export const getFieldKeyOrder = (keys: string[]): string[] => { + const sortedKeys = [...keys].sort(); + return ( + FieldKeyOrders.find((order) => isEqual([...order].sort(), sortedKeys)) ?? + keys + ); +}; + +export const isConfigMetaKey = (key: string) => + [ + 'kind', + 'llm_id', + 'global_rules', + 'example', + 'instruction', + 'synthesis', + 'use_blueprint', + 'mode', + 'rechunk', + 'rechunk_rules', + ].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.map((field) => + Object.fromEntries( + Object.entries(field).map(([key, value]) => [key, value ?? '']), + ), + ), + }; +}; + +export const buildConfigFromBuiltin = ( + builtinTemplate: ICompilationTemplateBuiltin, + kind: string, +): TemplateSchemaType['config'] => { + const instruction = + typeof builtinTemplate.config?.instruction === 'string' + ? builtinTemplate.config.instruction + : ''; + const example = + typeof builtinTemplate.config?.example === 'string' + ? builtinTemplate.config.example + : ''; + const sections: TemplateSchemaType['config'] = { + kind, + 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 && + (instruction.length > 0 || example.length > 0), + mode: + typeof builtinTemplate.config?.mode === 'string' + ? builtinTemplate.config.mode + : '', + ...(kind !== CompilationTemplateKind.Tree + ? { + rechunk: builtinTemplate.config?.rechunk === true, + rechunk_rules: + typeof builtinTemplate.config?.rechunk_rules === 'string' + ? builtinTemplate.config.rechunk_rules + : '', + } + : {}), + }; + + if ( + kind === CompilationTemplateKind.Artifacts && + (instruction.length > 0 || example.length > 0) + ) { + sections.instruction = instruction; + sections.example = example; + } + + if (kind === CompilationTemplateKind.Tree) { + const builtinRaptor: ICompilationTemplateRaptorConfig = + builtinTemplate.config?.raptor ?? {}; + return { + ...sections, + raptor: { + prompt: builtinRaptor.prompt ?? '', + claim_prompt: builtinRaptor.claim_prompt ?? '', + max_token: builtinRaptor.max_token ?? 512, + clustering_threshold: builtinRaptor.clustering_threshold ?? 0.3, + clustering_ratio: builtinRaptor.clustering_ratio ?? 0.5, + 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 storedInstruction = + typeof config.instruction === 'string' ? config.instruction : ''; + const storedExample = + typeof config.example === 'string' ? config.example : ''; + const hasBlueprintContent = Boolean( + storedInstruction.trim() || storedExample.trim(), + ); + const base: TemplateSchemaType['config'] = { + kind: config.kind ?? '', + global_rules: config.global_rules ?? '', + example: storedExample, + ...(typeof config.synthesis === 'object' && config.synthesis !== null + ? { + synthesis: + config.synthesis as TemplateSchemaType['config']['synthesis'], + } + : {}), + use_blueprint: + detail.kind === CompilationTemplateKind.Artifacts && hasBlueprintContent, + mode: typeof config.mode === 'string' ? config.mode : '', + ...(detail.kind !== CompilationTemplateKind.Tree + ? { + rechunk: config.rechunk === true, + rechunk_rules: + typeof config.rechunk_rules === 'string' + ? config.rechunk_rules + : '', + } + : {}), + }; + + if ( + detail.kind === CompilationTemplateKind.Artifacts && + hasBlueprintContent + ) { + base.instruction = storedInstruction; + base.example = storedExample; + } + + if (detail.kind === CompilationTemplateKind.Tree) { + const raptor: ICompilationTemplateRaptorConfig = config.raptor ?? {}; + return { + id: detail.id, + name: detail.name ?? '', + description: detail.description ?? '', + kind: detail.kind ?? '', + config: { + ...base, + raptor: { + prompt: raptor.prompt ?? '', + claim_prompt: raptor.claim_prompt ?? '', + max_token: raptor.max_token ?? 512, + clustering_threshold: raptor.clustering_threshold ?? 0.3, + clustering_ratio: raptor.clustering_ratio ?? 0.5, + 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 ?? '', + 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, + }; + + Object.entries(template.config).forEach(([key, value]) => { + if (key === 'kind') return; + if (key === 'example' || key === 'instruction') return; + if (key === 'synthesis') { + config[key] = value as ICompilationTemplateConfigRequest[string]; + return; + } + if (key === 'mode') { + config.mode = value as ICompilationTemplateConfigRequest['mode']; + return; + } + if (isConfigMetaKey(key)) { + if (typeof value === 'string' || typeof value === 'boolean') + 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 example = String(template.config.example ?? '').trim(); + config.instruction = instruction; + config.example = example; + } else { + config.instruction = ''; + 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 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 getTypeOptionsFromBuiltinSection = ( + builtinSection?: ICompilationTemplateSection, +) => { + const typeSet = new Set(); + builtinSection?.fields?.forEach((field) => { + if (field.type) typeSet.add(field.type); + }); + return Array.from(typeSet) + .sort() + .map((value) => ({ label: value, value })); +}; + +export const getRequiredFieldKeys = (fieldKeys: string[]) => + fieldKeys.includes('type') ? ['type', 'description'] : fieldKeys; + +export const getAvailableTypeOptions = ( + builtinSection: ICompilationTemplateSection | undefined, + existingFields: Record[] | undefined, + editingType?: string, +) => { + const usedTypes = new Set( + (existingFields ?? []).map((field) => field.type).filter(Boolean), + ); + if (editingType) usedTypes.delete(editingType); + return getTypeOptionsFromBuiltinSection(builtinSection).filter( + (option) => !usedTypes.has(option.value), + ); +}; diff --git a/web/src/components/create-compilation-template-group-dialog.tsx b/web/src/components/create-compilation-template-group-dialog.tsx new file mode 100644 index 0000000000..9ebde07ef5 --- /dev/null +++ b/web/src/components/create-compilation-template-group-dialog.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2026 The InfiniFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template'; +import { CompilationTemplateGroupEditor } from '@/components/compilation-template-group'; +import { CompilationTemplateGroupSaveResult } from '@/components/compilation-template-group/hooks/use-compilation-template-group-submit'; +import { CompilationTemplateGroupKeys } from '@/hooks/use-compilation-template-group-request'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type CreateCompilationTemplateGroupDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + onCreated: (group: ICompilationTemplateGroup) => void; +}; + +export function CreateCompilationTemplateGroupDialog({ + open, + onOpenChange, + onCreated, +}: CreateCompilationTemplateGroupDialogProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const handleSuccess = useCallback( + async (result: CompilationTemplateGroupSaveResult) => { + // Land the new group in the options cache before the field adopts its + // id, otherwise the select briefly renders it as a missing value. + await queryClient.invalidateQueries({ + queryKey: CompilationTemplateGroupKeys.all(), + }); + onOpenChange(false); + if (result.data) { + onCreated(result.data); + } + }, + [onCreated, onOpenChange, queryClient], + ); + + const handleCancel = useCallback(() => { + onOpenChange(false); + }, [onOpenChange]); + + return ( + + + + + {t('knowledgeCompilation.addTemplateGroup')} + + + + + + ); +} diff --git a/web/src/hooks/use-compilation-template-group-request.ts b/web/src/hooks/use-compilation-template-group-request.ts index c112f01d2a..3ed43051fd 100644 --- a/web/src/hooks/use-compilation-template-group-request.ts +++ b/web/src/hooks/use-compilation-template-group-request.ts @@ -33,7 +33,6 @@ import { isCreateCompilationTemplateGroup } from '@/utils/compilation-template-u import { markListItemsDeleted } from '@/utils/list-deletion-util'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; -import { useParams } from 'react-router'; import { AgentKeys } from './use-agent-request'; @@ -55,8 +54,7 @@ export const CompilationTemplateGroupKeys = { [CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups] as const, }; -export const useFetchCompilationTemplateGroup = () => { - const { id } = useParams<{ id: string }>(); +export const useFetchCompilationTemplateGroup = (id?: string) => { const isCreate = isCreateCompilationTemplateGroup(id); const { data, isFetching: loading } = useQuery< diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/index.tsx b/web/src/pages/user-setting/compilation-templates/edit-next/index.tsx index 6afd0d7d62..6f370562de 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/index.tsx +++ b/web/src/pages/user-setting/compilation-templates/edit-next/index.tsx @@ -15,25 +15,19 @@ */ import BackButton from '@/components/back-button'; -import { Button } from '@/components/ui/button'; -import { Form } from '@/components/ui/form'; -import { CompilationTemplateKind } from '@/constants/compilation'; +import { CompilationTemplateGroupEditor } from '@/components/compilation-template-group'; import { useFetchCompilationTemplateGroup } from '@/hooks/use-compilation-template-group-request'; -import { useCallback, useMemo } from 'react'; -import { useWatch } from 'react-hook-form'; +import { isCreateCompilationTemplateGroup } from '@/utils/compilation-template-util'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useSearchParams } from 'react-router'; +import { useNavigate, useParams, useSearchParams } 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'; import { resolveAgentsUrl } from './utils'; -const SelectedTemplateIndex = 0; - export default function EditNextCompilationTemplate() { const { t } = useTranslation(); const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); const [searchParams] = useSearchParams(); const agentsUrl = resolveAgentsUrl(searchParams.get('source')); @@ -41,23 +35,8 @@ export default function EditNextCompilationTemplate() { navigate(agentsUrl); }, [navigate, agentsUrl]); - 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], - ); + const isCreate = isCreateCompilationTemplateGroup(id); + const { data: group } = useFetchCompilationTemplateGroup(id); return (
@@ -70,33 +49,12 @@ export default function EditNextCompilationTemplate() { -
- - - {isArtifacts && ( - - )} - - -
- - -
-
- +
); } diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts b/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts index 3c3f0fbe9c..2b4f387446 100644 --- a/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts +++ b/web/src/pages/user-setting/compilation-templates/edit-next/utils.ts @@ -14,335 +14,10 @@ * limitations under the License. */ -import { isEqual } from 'lodash'; - import { AgentQuery } from '@/constants/agent'; -import { CompilationTemplateKind } from '@/constants/compilation'; import { AgentListItemType } from '@/interfaces/database/agent'; -import { - ICompilationTemplate, - ICompilationTemplateBuiltin, - ICompilationTemplateGroup, - ICompilationTemplateRaptorConfig, - ICompilationTemplateSection, -} from '@/interfaces/database/compilation-template'; -import { ICompilationTemplateConfigRequest } from '@/interfaces/request/compilation-template'; import { Routes } from '@/routes'; -import { FormSchemaType, TemplateSchemaType } from './schema'; -import { - DefaultTemplateValues, - FieldKeyOrders, - SectionPriority, -} from './constant'; - -export const getFieldKeyOrder = (keys: string[]): string[] => { - const sortedKeys = [...keys].sort(); - return ( - FieldKeyOrders.find((order) => isEqual([...order].sort(), sortedKeys)) ?? - keys - ); -}; - -export const isConfigMetaKey = (key: string) => - [ - 'kind', - 'llm_id', - 'global_rules', - 'example', - 'instruction', - 'synthesis', - 'use_blueprint', - 'mode', - 'rechunk', - 'rechunk_rules', - ].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.map((field) => - Object.fromEntries( - Object.entries(field).map(([key, value]) => [key, value ?? '']), - ), - ), - }; -}; - -export const buildConfigFromBuiltin = ( - builtinTemplate: ICompilationTemplateBuiltin, - kind: string, -): TemplateSchemaType['config'] => { - const instruction = - typeof builtinTemplate.config?.instruction === 'string' - ? builtinTemplate.config.instruction - : ''; - const example = - typeof builtinTemplate.config?.example === 'string' - ? builtinTemplate.config.example - : ''; - const sections: TemplateSchemaType['config'] = { - kind, - 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 && - (instruction.length > 0 || example.length > 0), - mode: - typeof builtinTemplate.config?.mode === 'string' - ? builtinTemplate.config.mode - : '', - ...(kind !== CompilationTemplateKind.Tree - ? { - rechunk: builtinTemplate.config?.rechunk === true, - rechunk_rules: - typeof builtinTemplate.config?.rechunk_rules === 'string' - ? builtinTemplate.config.rechunk_rules - : '', - } - : {}), - }; - - if ( - kind === CompilationTemplateKind.Artifacts && - (instruction.length > 0 || example.length > 0) - ) { - sections.instruction = instruction; - sections.example = example; - } - - if (kind === CompilationTemplateKind.Tree) { - const builtinRaptor: ICompilationTemplateRaptorConfig = - builtinTemplate.config?.raptor ?? {}; - return { - ...sections, - raptor: { - prompt: builtinRaptor.prompt ?? '', - claim_prompt: builtinRaptor.claim_prompt ?? '', - max_token: builtinRaptor.max_token ?? 512, - clustering_threshold: builtinRaptor.clustering_threshold ?? 0.3, - clustering_ratio: builtinRaptor.clustering_ratio ?? 0.5, - 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 storedInstruction = - typeof config.instruction === 'string' ? config.instruction : ''; - const storedExample = - typeof config.example === 'string' ? config.example : ''; - const hasBlueprintContent = Boolean( - storedInstruction.trim() || storedExample.trim(), - ); - const base: TemplateSchemaType['config'] = { - kind: config.kind ?? '', - global_rules: config.global_rules ?? '', - example: storedExample, - ...(typeof config.synthesis === 'object' && config.synthesis !== null - ? { - synthesis: - config.synthesis as TemplateSchemaType['config']['synthesis'], - } - : {}), - use_blueprint: - detail.kind === CompilationTemplateKind.Artifacts && hasBlueprintContent, - mode: typeof config.mode === 'string' ? config.mode : '', - ...(detail.kind !== CompilationTemplateKind.Tree - ? { - rechunk: config.rechunk === true, - rechunk_rules: - typeof config.rechunk_rules === 'string' - ? config.rechunk_rules - : '', - } - : {}), - }; - - if ( - detail.kind === CompilationTemplateKind.Artifacts && - hasBlueprintContent - ) { - base.instruction = storedInstruction; - base.example = storedExample; - } - - if (detail.kind === CompilationTemplateKind.Tree) { - const raptor: ICompilationTemplateRaptorConfig = config.raptor ?? {}; - return { - id: detail.id, - name: detail.name ?? '', - description: detail.description ?? '', - kind: detail.kind ?? '', - config: { - ...base, - raptor: { - prompt: raptor.prompt ?? '', - claim_prompt: raptor.claim_prompt ?? '', - max_token: raptor.max_token ?? 512, - clustering_threshold: raptor.clustering_threshold ?? 0.3, - clustering_ratio: raptor.clustering_ratio ?? 0.5, - 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 ?? '', - 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, - }; - - Object.entries(template.config).forEach(([key, value]) => { - if (key === 'kind') return; - if (key === 'example' || key === 'instruction') return; - if (key === 'synthesis') { - config[key] = value as ICompilationTemplateConfigRequest[string]; - return; - } - if (key === 'mode') { - config.mode = value as ICompilationTemplateConfigRequest['mode']; - return; - } - if (isConfigMetaKey(key)) { - if (typeof value === 'string' || typeof value === 'boolean') - 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 example = String(template.config.example ?? '').trim(); - config.instruction = instruction; - config.example = example; - } else { - config.instruction = ''; - 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 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 getTypeOptionsFromBuiltinSection = ( - builtinSection?: ICompilationTemplateSection, -) => { - const typeSet = new Set(); - builtinSection?.fields?.forEach((field) => { - if (field.type) typeSet.add(field.type); - }); - return Array.from(typeSet) - .sort() - .map((value) => ({ label: value, value })); -}; - -export const getRequiredFieldKeys = (fieldKeys: string[]) => - fieldKeys.includes('type') ? ['type', 'description'] : fieldKeys; - -export const getAvailableTypeOptions = ( - builtinSection: ICompilationTemplateSection | undefined, - existingFields: Record[] | undefined, - editingType?: string, -) => { - const usedTypes = new Set( - (existingFields ?? []).map((field) => field.type).filter(Boolean), - ); - if (editingType) usedTypes.delete(editingType); - return getTypeOptionsFromBuiltinSection(builtinSection).filter( - (option) => !usedTypes.has(option.value), - ); -}; - export const resolveAgentsUrl = (source: string | null): string => { // Only the Agents page owns the canvas_category tab param; other entry // points keep the plain Agents route on return.