- {loading && filteredNodes.length === 0 ? (
+
+ {loading && filteredTreeData.length === 0 ? (
- ) : filteredNodes.length === 0 ? (
+ ) : filteredTreeData.length === 0 ? (
{debouncedSearchString
? t('common.noData')
: t('datasetSkill.empty')}
) : (
-
- {filteredNodes.map((item) => (
-
- ))}
-
+
)}
diff --git a/web/src/pages/dataset/compilation/skills-view.tsx b/web/src/pages/dataset/compilation/skills-view.tsx
new file mode 100644
index 0000000000..a4029aab03
--- /dev/null
+++ b/web/src/pages/dataset/compilation/skills-view.tsx
@@ -0,0 +1,82 @@
+import MarkdownEditor from '@/components/markdown-editor';
+import { Card } from '@/components/ui/card';
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from '@/components/ui/resizable';
+import { DatasetSkillKeys } from '@/hooks/use-dataset-skill-request';
+import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
+import {
+ GenerateStatus,
+ GenerateType,
+} from '@/pages/dataset/dataset/generate-button/constants';
+import { useTraceRunData } from '@/pages/dataset/dataset/generate-button/hook';
+import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-generate-status';
+import { useQueryClient } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import { useParams } from 'react-router';
+
+import CompilationEmptyState from './empty-state';
+import { useCompilationSkill } from './hooks/use-compilation-skill';
+import { CompilationLoadingCard } from './loading-card';
+import { SkillsLeftPanel } from './skills-left-panel';
+
+export function SkillsView() {
+ const { id } = useParams();
+ const queryClient = useQueryClient();
+ const { data: knowledgeBase } = useFetchKnowledgeBaseConfiguration();
+ const {
+ selectedSkill,
+ setSelectedSkill,
+ skillTree,
+ skillTreeLoading,
+ skillPage,
+ } = useCompilationSkill();
+
+ const { data: skillRunData } = useTraceRunData(GenerateType.ToSkills);
+ const { status: skillStatus } = useGenerateStatus(skillRunData);
+
+ useEffect(() => {
+ if (skillStatus === GenerateStatus.completed) {
+ queryClient.invalidateQueries({
+ queryKey: DatasetSkillKeys.tree(id!),
+ });
+ }
+ }, [skillStatus, queryClient, id]);
+
+ const canGenerate = (knowledgeBase?.chunk_count ?? 0) > 0;
+ const isLoading = skillTreeLoading && !skillTree?.skill_with_weight?.length;
+ const isEmpty = !skillTree?.skill_with_weight?.length && !skillTreeLoading;
+
+ if (isLoading) {
+ return
;
+ }
+
+ if (isEmpty) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/pages/dataset/compilation/utils/skill-tree.ts b/web/src/pages/dataset/compilation/utils/skill-tree.ts
new file mode 100644
index 0000000000..4eca4afc72
--- /dev/null
+++ b/web/src/pages/dataset/compilation/utils/skill-tree.ts
@@ -0,0 +1,65 @@
+import type { TreeDataItem } from '@/components/ui/tree-view';
+import type { DatasetSkillTreeNode } from '@/interfaces/database/dataset-skill';
+import type { ReactNode } from 'react';
+
+export type SkillTreeActionsFactory = (skillKwd: string) => ReactNode;
+
+export function buildSkillTreeData(
+ nodes: DatasetSkillTreeNode[] = [],
+ getActions?: SkillTreeActionsFactory,
+): TreeDataItem[] {
+ return nodes.map((node) => {
+ const item: TreeDataItem = {
+ id: node.skill_kwd,
+ name: node.skill_kwd,
+ actions: getActions?.(node.skill_kwd),
+ };
+ // Only set children when non-empty: TreeView branches on
+ // `item.children ? TreeNode : TreeLeaf` and [] is truthy, so an empty
+ // array would render a leaf as a branch with a chevron and no children.
+ if (node.children_kwd?.length) {
+ item.children = buildSkillTreeData(node.children_kwd, getActions);
+ }
+ return item;
+ });
+}
+
+// Same prune logic as filterTreeDataByKeyword in
+// pages/chunk/representation/utils/adapters.ts — that module carries
+// chunk-specific TreeDataItem augmentation, so a feature-local copy is kept
+// here instead of a cross-feature import.
+export function filterSkillTreeData(
+ items: TreeDataItem[],
+ keyword: string,
+): TreeDataItem[] {
+ const normalizedKeyword = keyword.trim().toLowerCase();
+ if (!normalizedKeyword) {
+ return items;
+ }
+
+ return items.reduce
((acc, item) => {
+ const children = item.children
+ ? filterSkillTreeData(item.children, keyword)
+ : [];
+ const matches = item.name.toLowerCase().includes(normalizedKeyword);
+
+ if (matches || children.length > 0) {
+ acc.push({
+ ...item,
+ // A matched node keeps its full original subtree; an unmatched
+ // ancestor keeps only the pruned children leading to matches.
+ children: children.length > 0 ? children : item.children,
+ });
+ }
+ return acc;
+ }, []);
+}
+
+export function countSkillTreeNodes(
+ nodes: DatasetSkillTreeNode[] = [],
+): number {
+ return nodes.reduce(
+ (total, node) => total + 1 + countSkillTreeNodes(node.children_kwd),
+ 0,
+ );
+}
diff --git a/web/src/pages/dataset/dataset-setting/configuration/naive.tsx b/web/src/pages/dataset/dataset-setting/configuration/naive.tsx
index aaa286c87b..9d2bedae51 100644
--- a/web/src/pages/dataset/dataset-setting/configuration/naive.tsx
+++ b/web/src/pages/dataset/dataset-setting/configuration/naive.tsx
@@ -3,7 +3,6 @@ import {
AutoQuestionsFormField,
} from '@/components/auto-keywords-form-field';
import { ChildrenDelimiterForm } from '@/components/children-delimiter-form';
-import { CompilationTemplateFormField } from '@/components/compilation-template-form-field';
import { DelimiterFormField } from '@/components/delimiter-form-field';
import { ExcelToHtmlFormField } from '@/components/excel-to-html-form-field';
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
@@ -25,7 +24,6 @@ export function NaiveConfiguration() {
return (
-
{
+const TraceTypeMap: Record = {
+ [GenerateType.KnowledgeGraph]: TraceType.Graph,
+ [GenerateType.Raptor]: TraceType.Raptor,
+ [GenerateType.Artifact]: TraceType.Artifact,
+ [GenerateType.ToSkills]: TraceType.Skill,
+};
+
+export const useTraceRunData = (type: GenerateType) => {
const { id } = useParams();
- const { data: graphRunData, isFetching: graphRunLoading } = useTraceQuery(
- GenerateType.KnowledgeGraph,
- TraceType.Graph,
- open,
- id,
- );
- const { data: raptorRunData, isFetching: raptorRunLoading } = useTraceQuery(
- GenerateType.Raptor,
- TraceType.Raptor,
- open,
- id,
- );
-
- const { data: artifactRunData, isFetching: artifactRunLoading } =
- useTraceQuery(GenerateType.Artifact, TraceType.Artifact, open, id);
- const { data: skillRunData, isFetching: skillRunLoading } = useTraceQuery(
- GenerateType.ToSkills,
- TraceType.Skill,
- open,
- id,
- );
-
- return {
- graphRunData,
- graphRunLoading,
- raptorRunData,
- raptorRunLoading,
- artifactRunData,
- artifactRunLoading,
- skillRunData,
- skillRunLoading,
- };
+ return useTraceQuery(type, TraceTypeMap[type], true, id);
};
export const useUnBindTask = () => {
@@ -144,15 +120,7 @@ export const useDatasetGenerate = () => {
} = useMutation({
mutationKey: [DatasetKey.generate],
mutationFn: async ({ type }: { type: GenerateType }) => {
- const indexType =
- type === GenerateType.KnowledgeGraph
- ? TraceType.Graph
- : type === GenerateType.Artifact
- ? TraceType.Artifact
- : type === GenerateType.ToSkills
- ? TraceType.Skill
- : TraceType.Raptor;
- const { data } = await runIndex(id!, indexType);
+ const { data } = await runIndex(id!, TraceTypeMap[type]);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({
diff --git a/web/src/pages/dataset/sidebar/index.tsx b/web/src/pages/dataset/sidebar/index.tsx
index 3ba4ab890e..3a224130e2 100644
--- a/web/src/pages/dataset/sidebar/index.tsx
+++ b/web/src/pages/dataset/sidebar/index.tsx
@@ -68,7 +68,7 @@ export function SideBar({ dataset: data }: PropType) {
: []),
{
icon: ,
- label: t(`knowledgeDetails.compilation`),
+ label: 'Artifacts',
key: Routes.Compilation,
},
];
diff --git a/web/src/pages/user-setting/compilation-templates/create-next/components/add-field-modal.tsx b/web/src/pages/user-setting/compilation-templates/create-next/components/add-field-modal.tsx
index b9620c9da5..25e1b3f49c 100644
--- a/web/src/pages/user-setting/compilation-templates/create-next/components/add-field-modal.tsx
+++ b/web/src/pages/user-setting/compilation-templates/create-next/components/add-field-modal.tsx
@@ -96,6 +96,7 @@ export function AddFieldModal({
handleTypeChange(value);
}}
placeholder={t('setting.selectFieldType')}
+ allowCustomValue
/>
)}
@@ -105,7 +106,8 @@ export function AddFieldModal({
))}
diff --git a/web/src/pages/user-setting/compilation-templates/create-next/components/basic-info-step.tsx b/web/src/pages/user-setting/compilation-templates/create-next/components/basic-info-step.tsx
index d668293295..7a818df329 100644
--- a/web/src/pages/user-setting/compilation-templates/create-next/components/basic-info-step.tsx
+++ b/web/src/pages/user-setting/compilation-templates/create-next/components/basic-info-step.tsx
@@ -23,7 +23,11 @@ export function BasicInfoStep() {
label={t('setting.groupDescription')}
horizontal
>
-
+
diff --git a/web/src/pages/user-setting/compilation-templates/create-next/components/section-field-grid.tsx b/web/src/pages/user-setting/compilation-templates/create-next/components/section-field-grid.tsx
index 3fc7100bc4..677be7f734 100644
--- a/web/src/pages/user-setting/compilation-templates/create-next/components/section-field-grid.tsx
+++ b/web/src/pages/user-setting/compilation-templates/create-next/components/section-field-grid.tsx
@@ -1,25 +1,14 @@
import { Card, CardContent } from '@/components/ui/card';
-import { ICompilationTemplateSection } from '@/interfaces/database/compilation-template';
import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react';
-import { useMemo } from 'react';
-import {
- ArrayPath,
- useFieldArray,
- useFormContext,
- useWatch,
-} from 'react-hook-form';
+import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { FormSchemaType } from '@/pages/user-setting/compilation-templates/create-next/schema';
-import { getTypeOptionsFromBuiltinSection } from '@/pages/user-setting/compilation-templates/create-next/utils';
-
import { FieldCard } from './field-card';
type SectionFieldGridProps = {
- fieldsPath: ArrayPath;
+ fieldsPath: string;
sectionName: string;
- builtinSection?: ICompilationTemplateSection;
onOpenAddField: () => void;
onEditField: (index: number) => void;
};
@@ -27,12 +16,11 @@ type SectionFieldGridProps = {
export function SectionFieldGrid({
fieldsPath,
sectionName,
- builtinSection,
onOpenAddField,
onEditField,
}: SectionFieldGridProps) {
const { t } = useTranslation();
- const form = useFormContext();
+ const form = useFormContext();
const { fields, remove } = useFieldArray({
control: form.control,
name: fieldsPath,
@@ -40,29 +28,11 @@ export function SectionFieldGrid({
const isTypedSection = sectionName === 'entity' || sectionName === 'relation';
- const availableTypes = useMemo(
- () => getTypeOptionsFromBuiltinSection(builtinSection),
- [builtinSection],
- );
-
const currentFields = useWatch({
control: form.control,
name: fieldsPath,
}) as Record[] | undefined;
- const allTypesUsed = useMemo(() => {
- if (!isTypedSection || availableTypes.length === 0) return false;
- const availableSet = new Set(availableTypes.map((type) => type.value));
- const usedAvailableTypes = new Set(
- (currentFields ?? [])
- .map((field) => field.type)
- .filter(
- (type): type is string => Boolean(type) && availableSet.has(type),
- ),
- );
- return usedAvailableTypes.size >= availableTypes.length;
- }, [isTypedSection, availableTypes, currentFields]);
-
return (
{fields.map((field, index) => {
@@ -78,27 +48,25 @@ export function SectionFieldGrid({
);
})}
- {!allTypesUsed && (
- {
- 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',
- )}
- >
-
-
- {t('setting.addField')}
-
-
- )}
+ {
+ 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',
+ )}
+ >
+
+
+ {t('setting.addField')}
+
+
);
}
diff --git a/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx b/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx
index 9ce473974a..14fb154918 100644
--- a/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx
+++ b/web/src/pages/user-setting/compilation-templates/create-next/components/template-configuration.tsx
@@ -116,7 +116,6 @@ export function TemplateConfiguration({
key={activeFieldsPath}
fieldsPath={activeFieldsPath}
sectionName={sectionName}
- builtinSection={builtinSection}
onOpenAddField={handleOpenAddField}
onEditField={handleOpenEditField}
/>
@@ -126,7 +125,6 @@ export function TemplateConfiguration({
[
activeFieldsPath,
activeSectionTab,
- builtinSection,
handleOpenAddField,
handleOpenEditField,
],
@@ -147,6 +145,7 @@ export function TemplateConfiguration({
@@ -158,6 +157,7 @@ export function TemplateConfiguration({
@@ -189,7 +189,8 @@ export function TemplateConfiguration({
>
diff --git a/web/src/pages/user-setting/compilation-templates/create-next/schema.ts b/web/src/pages/user-setting/compilation-templates/create-next/schema.ts
index 9500466315..565cd9b0cf 100644
--- a/web/src/pages/user-setting/compilation-templates/create-next/schema.ts
+++ b/web/src/pages/user-setting/compilation-templates/create-next/schema.ts
@@ -28,7 +28,7 @@ export const buildSynthesisSchema = () =>
export const buildTemplateSchema = (t: (key: string) => string) =>
z.object({
id: z.string().optional(),
- name: 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')),
diff --git a/web/src/services/dataset-skill-service.ts b/web/src/services/dataset-skill-service.ts
index 963b029822..26e1ec9183 100644
--- a/web/src/services/dataset-skill-service.ts
+++ b/web/src/services/dataset-skill-service.ts
@@ -6,6 +6,12 @@ const datasetSkillService = {
request.get(api.getDatasetSkillTree(params.datasetId)),
getPage: (params: { datasetId: string; skillKwd: string }) =>
request.get(api.getDatasetSkillPage(params.datasetId, params.skillKwd)),
+ deleteTree: (params: { datasetId: string }) =>
+ request.delete(api.deleteDatasetSkillTree(params.datasetId)),
+ deletePage: (params: { datasetId: string; skillKwd: string }) =>
+ request.delete(
+ api.deleteDatasetSkillPage(params.datasetId, params.skillKwd),
+ ),
};
export default datasetSkillService;
diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts
index a63034197d..32ea4ad877 100644
--- a/web/src/utils/api.ts
+++ b/web/src/utils/api.ts
@@ -174,6 +174,13 @@ export default {
.split('/')
.map((s) => encodeURIComponent(s))
.join('/')}`,
+ deleteDatasetSkillTree: (datasetId: string) =>
+ `${restAPIv1}/datasets/${datasetId}/skills`,
+ deleteDatasetSkillPage: (datasetId: string, skillKwd: string) =>
+ `${restAPIv1}/datasets/${datasetId}/skills/${skillKwd
+ .split('/')
+ .map((s) => encodeURIComponent(s))
+ .join('/')}`,
// data pipeline log
fetchDataPipelineLog: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/ingestions`,