From 392b249404b8fa9922a7d5c0ea0546b234e0a719 Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 20 Jul 2026 19:00:08 +0800 Subject: [PATCH] Feat: Render the skills list using a tree view. (#17115) ### Summary Feat: Render the skills list using a tree view. --- web/CLAUDE.md | 8 + .../components/chunk-method-dialog/index.tsx | 6 - .../use-default-parser-values.ts | 1 - .../compilation-template-form-field.tsx | 97 +---- web/src/components/markdown-editor.tsx | 4 +- web/src/hooks/use-dataset-skill-request.ts | 57 ++- web/src/locales/en.ts | 8 +- web/src/locales/zh.ts | 7 +- web/src/main.tsx | 4 +- .../node/dropdown/accordion-operators.tsx | 6 +- .../agent/form/compilation-form/index.tsx | 2 +- .../components/document-view-switch/index.tsx | 2 +- .../pages/dataset/compilation/constants.ts | 1 - .../compilation/hooks/use-compilation-view.ts | 67 ---- web/src/pages/dataset/compilation/index.tsx | 146 +------- .../compilation/knowledge-force-graph.tsx | 347 ------------------ .../dataset/compilation/llm-wiki-view.tsx | 104 ++++++ .../dataset/compilation/loading-card.tsx | 10 + .../dataset/compilation/skills-left-panel.tsx | 207 +++++++---- .../pages/dataset/compilation/skills-view.tsx | 82 +++++ .../dataset/compilation/utils/skill-tree.ts | 65 ++++ .../dataset-setting/configuration/naive.tsx | 2 - .../dataset/dataset-setting/form-schema.ts | 1 - .../pages/dataset/dataset-setting/index.tsx | 1 - .../dataset/dataset/generate-button/hook.ts | 52 +-- web/src/pages/dataset/sidebar/index.tsx | 2 +- .../components/add-field-modal.tsx | 4 +- .../components/basic-info-step.tsx | 6 +- .../components/section-field-grid.tsx | 76 ++-- .../components/template-configuration.tsx | 7 +- .../create-next/schema.ts | 2 +- web/src/services/dataset-skill-service.ts | 6 + web/src/utils/api.ts | 7 + 33 files changed, 571 insertions(+), 826 deletions(-) delete mode 100644 web/src/pages/dataset/compilation/hooks/use-compilation-view.ts delete mode 100644 web/src/pages/dataset/compilation/knowledge-force-graph.tsx create mode 100644 web/src/pages/dataset/compilation/llm-wiki-view.tsx create mode 100644 web/src/pages/dataset/compilation/loading-card.tsx create mode 100644 web/src/pages/dataset/compilation/skills-view.tsx create mode 100644 web/src/pages/dataset/compilation/utils/skill-tree.ts diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 4a755ac752..fed624cae5 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -81,6 +81,14 @@ For React Query / cache invalidation bugs, **carefully compare query keys across - Systematically: (1) list every component/hook that calls `useQuery` for this data, (2) compare their query keys character-for-character, (3) check every mutation's `onSuccess` for cache invalidation, and (4) verify no parent re-renders are remounting the observer. +#### Colocate Queries with the Consuming View + +**Fire a query in the component that renders its data — not in a parent page.** When a page switches between mutually exclusive views (tabs, view modes), extract each view into its own component that issues its own requests on mount. Conditional rendering then provides lazy loading for free. + +- Do not hoist child-view queries into the page component — it fires requests the user may never need (e.g., fetching the skill tree on page entry while the default view is the LLM wiki). +- Do not thread `enabled` flags or view-mode props through hooks to gate a hoisted query; that is a sign the query lives at the wrong level. Split the view instead. +- Remember the trade-off: with `gcTime: 0`, unmounting a view drops its cache, so switching back refetches. That is usually desirable for always-fresh data — do not reintroduce eager hoisting just to avoid the refetch. + ### Network Request Layering HTTP requests are organized in three layers. **Never import `@/utils/request`, `@/utils/next-request`, or `@/utils/api` directly inside a hook**: diff --git a/web/src/components/chunk-method-dialog/index.tsx b/web/src/components/chunk-method-dialog/index.tsx index f9b117e731..63aaec7503 100644 --- a/web/src/components/chunk-method-dialog/index.tsx +++ b/web/src/components/chunk-method-dialog/index.tsx @@ -37,7 +37,6 @@ import { AutoQuestionsFormField, } from '../auto-keywords-form-field'; import { ChildrenDelimiterForm } from '../children-delimiter-form'; -import { CompilationTemplateFormField } from '../compilation-template-form-field'; import { DataFlowSelect } from '../data-pipeline-select'; import { DelimiterFormField } from '../delimiter-form-field'; import { EntityTypesFormField } from '../entity-types-form-field'; @@ -153,7 +152,6 @@ export function ChunkMethodDialog({ ) .optional(), enable_metadata: z.boolean().optional(), - compilation_template_group_id: z.array(z.string()).optional(), }), }) .superRefine((data, ctx) => { @@ -302,10 +300,6 @@ export function ChunkMethodDialog({ {parseType === ParseType.BuiltIn && } - {parseType === ParseType.BuiltIn && ( - - )} - {showPages && parseType === ParseType.BuiltIn && ( )} diff --git a/web/src/components/chunk-method-dialog/use-default-parser-values.ts b/web/src/components/chunk-method-dialog/use-default-parser-values.ts index 5e0a1d189e..b80d80b902 100644 --- a/web/src/components/chunk-method-dialog/use-default-parser-values.ts +++ b/web/src/components/chunk-method-dialog/use-default-parser-values.ts @@ -42,7 +42,6 @@ export function useDefaultParserValues() { metadata: [], built_in_metadata: [], enable_metadata: false, - compilation_template_group_id: [], }; return defaultParserValues as IParserConfig; diff --git a/web/src/components/compilation-template-form-field.tsx b/web/src/components/compilation-template-form-field.tsx index 7407bffcfe..4e0ce33df1 100644 --- a/web/src/components/compilation-template-form-field.tsx +++ b/web/src/components/compilation-template-form-field.tsx @@ -1,103 +1,25 @@ import { RAGFlowFormItem } from '@/components/ragflow-form'; -import { - MultiSelect, - MultiSelectOptionType, -} from '@/components/ui/multi-select'; import { useFetchAllCompilationTemplateGroups } from '@/hooks/use-compilation-template-group-request'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { SelectWithSearch } from './originui/select-with-search'; type CompilationTemplateFormFieldProps = { horizontal?: boolean; name?: string; }; -const ScopeTranslationKeyMap: Record = { - file: 'scopeFile', - dataset: 'scopeDataset', -}; - -type CompilationTemplateMultiSelectProps = { - value?: string[]; - onChange(value: string[]): void; -}; - -/** - * Renders a multi-select for compilation template groups. - * - * Selection rule: - * - Each group has a scope: "file" or "dataset". - * - At most one group per scope can be selected. - * - The two scopes can be combined (one file group + one dataset group). - * - Once a scope is selected, remaining options of the same scope are disabled. - * - The scope is displayed as a suffix after each option label. - */ -function CompilationTemplateMultiSelect({ - value: rawValue = [], - onChange, -}: CompilationTemplateMultiSelectProps) { - const { t } = useTranslation(); - const { groups } = useFetchAllCompilationTemplateGroups(); - - const value = useMemo(() => { - // Normalize legacy single-string values into an array during the migration. - if (Array.isArray(rawValue)) return rawValue; - if (typeof rawValue === 'string' && rawValue) return [rawValue]; - return []; - }, [rawValue]); - - // Collect scopes that are already selected by the current value. - const selectedScopes = useMemo(() => { - const scopes = new Set(); - value.forEach((id) => { - const group = groups?.find((g) => g.id === id); - if (group?.scope) { - scopes.add(group.scope); - } - }); - return scopes; - }, [value, groups]); - - const options = useMemo(() => { - return (groups ?? []).map((group) => { - const scopeTranslationKey = group.scope - ? ScopeTranslationKeyMap[group.scope] - : undefined; - // Disable other options that share an already-selected scope. - const isSameScopeSelected = - !!group.scope && - selectedScopes.has(group.scope) && - !value.includes(group.id); - - return { - label: group.name, - value: group.id, - disabled: isSameScopeSelected, - suffix: scopeTranslationKey ? ( - - ({t(`knowledgeConfiguration.${scopeTranslationKey}`)}) - - ) : undefined, - }; - }); - }, [groups, selectedScopes, value, t]); - - return ( - - ); -} - export function CompilationTemplateFormField({ horizontal, name = 'parser_config.compilation_template_group_id', }: CompilationTemplateFormFieldProps) { const { t } = useTranslation(); + const { groups } = useFetchAllCompilationTemplateGroups(); + + const options = useMemo( + () => groups?.map((group) => ({ label: group.name, value: group.id })), + [groups], + ); return ( {(field) => ( - )} diff --git a/web/src/components/markdown-editor.tsx b/web/src/components/markdown-editor.tsx index 357320bb1c..bd96cdb638 100644 --- a/web/src/components/markdown-editor.tsx +++ b/web/src/components/markdown-editor.tsx @@ -21,7 +21,9 @@ export default function MarkdownEditor({ const contentRef = useRef(content); useEffect(() => { - contentRef.current = content; + if (content === contentRef.current) { + return; + } if (showSource) setRawContent(content); }, [showSource, content]); diff --git a/web/src/hooks/use-dataset-skill-request.ts b/web/src/hooks/use-dataset-skill-request.ts index b6ac07cb7c..755da3deb7 100644 --- a/web/src/hooks/use-dataset-skill-request.ts +++ b/web/src/hooks/use-dataset-skill-request.ts @@ -1,9 +1,11 @@ +import message from '@/components/ui/message'; import { DatasetSkillPage, DatasetSkillTree, } from '@/interfaces/database/dataset-skill'; +import i18n from '@/locales/config'; import datasetSkillService from '@/services/dataset-skill-service'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useKnowledgeBaseId } from './use-knowledge-request'; @@ -51,3 +53,56 @@ export function useFetchDatasetSkillPage(skillKwd: string | null | undefined) { return { data, loading }; } + +export function useDeleteDatasetSkillTree() { + const kbId = useKnowledgeBaseId(); + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationFn: async () => { + const { data } = await datasetSkillService.deleteTree({ + datasetId: kbId, + }); + if (data?.code === 0) { + message.success(i18n.t('message.deleted')); + queryClient.invalidateQueries({ + queryKey: DatasetSkillKeys.all(kbId), + }); + } + return data; + }, + }); + + return { data, loading, deleteSkillTree: mutateAsync }; +} + +export function useDeleteDatasetSkillPage() { + const kbId = useKnowledgeBaseId(); + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationFn: async (skillKwd: string) => { + const { data } = await datasetSkillService.deletePage({ + datasetId: kbId, + skillKwd, + }); + if (data?.code === 0) { + message.success(i18n.t('message.deleted')); + queryClient.invalidateQueries({ + queryKey: DatasetSkillKeys.all(kbId), + }); + } + return data; + }, + }); + + return { data, loading, deleteSkillPage: mutateAsync }; +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 78c6629b13..96399fc740 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -517,7 +517,6 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim graph: 'Graph', graphPlaceholder: 'Graph view placeholder', llmWiki: 'LLM Wiki', - skills: 'Skills', contents: 'Navigation', topics: 'Topics', concept: 'Concept', @@ -1013,7 +1012,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s }, representationUnsupported: 'This representation type is not supported yet.', - representationEmpty: 'No representation templates available.', + representationEmpty: 'No artifact templates available.', enable: 'Enable', disable: 'Disable', delete: 'Delete', @@ -2066,6 +2065,11 @@ Example: Virtual Hosted Style`, selectFolder: 'Select a skill to view details', currentFolder: 'Skill', noContent: 'No content', + deleteAllTitle: 'Delete all skills', + deleteAllDescription: + 'Are you sure you want to delete all skills? This action cannot be undone.', + deleteSkillTitle: 'Delete skill', + deleteSkillDescription: 'Are you sure you want to delete this skill?', }, message: { registered: 'Registered!', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index d31662ca54..d24dfbb530 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -463,7 +463,6 @@ export default { graph: '图谱', graphPlaceholder: '图谱视图占位', llmWiki: 'LLM Wiki', - skills: '技能', contents: '导航', topics: '主题', concept: '概念', @@ -914,7 +913,7 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 raptor: 'RAPTOR', }, representationUnsupported: '暂不支持该表征类型。', - representationEmpty: '暂无表征模板。', + representationEmpty: '暂无Artifact模板。', enable: '启用', disable: '禁用', delete: '删除', @@ -1728,6 +1727,10 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 selectFolder: '选择一个技能以查看详情', currentFolder: '技能', noContent: '无内容', + deleteAllTitle: '删除全部技能', + deleteAllDescription: '确定要删除全部技能吗?此操作无法撤销。', + deleteSkillTitle: '删除技能', + deleteSkillDescription: '确定要删除该技能吗?', }, message: { registered: '注册成功', diff --git a/web/src/main.tsx b/web/src/main.tsx index 8b59f5551b..90901d4112 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { gotoVSCode, Inspector } from 'react-dev-inspector'; +import { Inspector } from 'react-dev-inspector'; import ReactDOM from 'react-dom/client'; import '../tailwind.css'; import App from './app'; @@ -9,7 +9,7 @@ import { initLanguage } from './locales/config'; initLanguage().then(() => { ReactDOM.createRoot(document.getElementById('root')!).render( - + , ); diff --git a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx index 475240b0d5..b19b4a7b6d 100644 --- a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx +++ b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx @@ -173,9 +173,11 @@ export function PipelineAccordionOperators({ ...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]), ]; list.push(Operator.Extractor); - list.push(Operator.Compilation); + if (getOperatorTypeFromId(nodeId) !== Operator.Compilation) { + list.push(Operator.Compilation); + } return list; - }, [restrictSingleOperatorOnCanvas]); + }, [getOperatorTypeFromId, nodeId, restrictSingleOperatorOnCanvas]); const chunkerOperators = useMemo(() => { return [ diff --git a/web/src/pages/agent/form/compilation-form/index.tsx b/web/src/pages/agent/form/compilation-form/index.tsx index 5c08a5b6f0..49caf0da53 100644 --- a/web/src/pages/agent/form/compilation-form/index.tsx +++ b/web/src/pages/agent/form/compilation-form/index.tsx @@ -16,7 +16,7 @@ import { FormWrapper } from '../components/form-wrapper'; import { Output } from '../components/output'; export const FormSchema = z.object({ - compilation_template_group_ids: z.array(z.string()).optional(), + compilation_template_group_ids: z.string().optional(), ...LlmSettingSchema, }); diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-view-switch/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-view-switch/index.tsx index a6949820f1..44118464b0 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-view-switch/index.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-view-switch/index.tsx @@ -70,7 +70,7 @@ export default function DocumentViewSwitch({ label: (
- {t('chunk.representation', 'Representation')} + Artifact
), }, diff --git a/web/src/pages/dataset/compilation/constants.ts b/web/src/pages/dataset/compilation/constants.ts index 387f011e22..10981bf5b9 100644 --- a/web/src/pages/dataset/compilation/constants.ts +++ b/web/src/pages/dataset/compilation/constants.ts @@ -1,5 +1,4 @@ export enum ViewMode { - Graph = 'graph', LlmWiki = 'llm-wiki', Skills = 'skills', } diff --git a/web/src/pages/dataset/compilation/hooks/use-compilation-view.ts b/web/src/pages/dataset/compilation/hooks/use-compilation-view.ts deleted file mode 100644 index 4a529e0521..0000000000 --- a/web/src/pages/dataset/compilation/hooks/use-compilation-view.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { DatasetSkillKeys } from '@/hooks/use-dataset-skill-request'; -import { ArtifactKeys, ArtifactTopicKeys } from '@/hooks/use-knowledge-request'; -import { useTraceGenerate } 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 { useCallback, useEffect, useState } from 'react'; -import { useParams } from 'react-router'; -import { LeftPanelTab, ViewMode } from '../constants'; - -export function useCompilationView() { - const { id } = useParams(); - const queryClient = useQueryClient(); - const [leftTab, setLeftTab] = useState(LeftPanelTab.Contents); - const [viewMode, setViewMode] = useState(ViewMode.LlmWiki); - - const { artifactRunData, skillRunData } = useTraceGenerate({ open: true }); - const { status: artifactStatus } = useGenerateStatus(artifactRunData); - const { status: skillStatus } = useGenerateStatus(skillRunData); - - useEffect(() => { - if (viewMode === ViewMode.LlmWiki && artifactStatus === 'completed') { - queryClient.invalidateQueries({ - queryKey: ArtifactKeys.listByDataset(id!), - }); - queryClient.invalidateQueries({ - queryKey: ArtifactTopicKeys.listByDataset(id!), - }); - } - }, [viewMode, artifactStatus, queryClient, id]); - - useEffect(() => { - if (viewMode === ViewMode.Skills && skillStatus === 'completed') { - queryClient.invalidateQueries({ - queryKey: DatasetSkillKeys.tree(id!), - }); - } - }, [viewMode, skillStatus, queryClient, id]); - - const handleSwitchToGraph = useCallback(() => { - setViewMode(ViewMode.Graph); - }, []); - - const handleSwitchToLlmWiki = useCallback(() => { - setViewMode(ViewMode.LlmWiki); - }, []); - - const handleSwitchToSkills = useCallback(() => { - setViewMode(ViewMode.Skills); - }, []); - - const handleLeftTabChange = useCallback((value: string) => { - setLeftTab(value as LeftPanelTab); - }, []); - - return { - leftTab, - viewMode, - artifactRunData, - skillRunData, - artifactStatus, - skillStatus, - handleSwitchToGraph, - handleSwitchToLlmWiki, - handleSwitchToSkills, - handleLeftTabChange, - }; -} diff --git a/web/src/pages/dataset/compilation/index.tsx b/web/src/pages/dataset/compilation/index.tsx index e9fa3aa1a0..a2b4ae90dd 100644 --- a/web/src/pages/dataset/compilation/index.tsx +++ b/web/src/pages/dataset/compilation/index.tsx @@ -1,83 +1,30 @@ import BackButton from '@/components/back-button'; -import MarkdownEditor from '@/components/markdown-editor'; import { RAGFlowAvatar } from '@/components/ragflow-avatar'; -import { SkeletonCard } from '@/components/skeleton-card'; import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from '@/components/ui/resizable'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; -import { - useFetchArtifactTopicList, - useFetchKnowledgeBaseConfiguration, - useFetchKnowledgeGraph, -} from '@/hooks/use-knowledge-request'; +import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { ViewMode } from './constants'; -import CompilationEmptyState from './empty-state'; -import { useCompilationArtifact } from './hooks/use-compilation-artifact'; -import { useCompilationSkill } from './hooks/use-compilation-skill'; -import { useCompilationView } from './hooks/use-compilation-view'; -import KnowledgeForceGraph from './knowledge-force-graph'; -import { SkillsLeftPanel } from './skills-left-panel'; -import { WikiDetailContent } from './wiki-detail-content'; -import { WikiLeftPanel } from './wiki-left-panel'; +import { LlmWikiView } from './llm-wiki-view'; +import { SkillsView } from './skills-view'; export default function Compilation() { const { t } = useTranslation(); const { id } = useParams(); const { navigateToDataFile } = useNavigatePage(); const { data: knowledgeBase } = useFetchKnowledgeBaseConfiguration(); - const { data: knowledgeGraph, loading: knowledgeGraphLoading } = - useFetchKnowledgeGraph(); - const { topics, loading: topicListLoading } = useFetchArtifactTopicList(); + const [viewMode, setViewMode] = useState(ViewMode.LlmWiki); - const { - leftTab, - viewMode, - artifactRunData, - skillRunData, - handleSwitchToLlmWiki, - handleSwitchToSkills, - handleLeftTabChange, - } = useCompilationView(); + const handleSwitchToLlmWiki = useCallback(() => { + setViewMode(ViewMode.LlmWiki); + }, []); - const { - selectedArtifact, - selectedVersion, - selectVersion, - handleSelectArtifact, - clearSelectedArtifact, - } = useCompilationArtifact(); - - const { - selectedSkill, - setSelectedSkill, - skillTree, - skillTreeLoading, - skillPage, - } = useCompilationSkill(); - - const isLlmWikiEmpty = topics.length === 0 && !topicListLoading; - const canGenerate = (knowledgeBase?.chunk_count ?? 0) > 0; - - const isGraphLoading = knowledgeGraphLoading && !knowledgeGraph?.graph; - const isLlmWikiLoading = topicListLoading && topics.length === 0; - const isSkillsLoading = - skillTreeLoading && !skillTree?.skill_with_weight?.length; - const isSkillsEmpty = - !skillTree?.skill_with_weight?.length && !skillTreeLoading; - - const loadingCard = ( - - - - ); + const handleSwitchToSkills = useCallback(() => { + setViewMode(ViewMode.Skills); + }, []); return (
@@ -112,80 +59,13 @@ export default function Compilation() { size="sm" onClick={handleSwitchToSkills} > - {t('knowledgeDetails.skills')} + To Skills
- {viewMode === ViewMode.Graph ? ( - isGraphLoading ? ( - loadingCard - ) : ( -
- -
- ) - ) : viewMode === ViewMode.LlmWiki ? ( - isLlmWikiLoading ? ( - loadingCard - ) : isLlmWikiEmpty ? ( - - ) : ( - - - - - - - - - - - - ) - ) : isSkillsLoading ? ( - loadingCard - ) : isSkillsEmpty ? ( - - ) : ( - - - - - - - - - - - - )} + {viewMode === ViewMode.LlmWiki ? : } ); } diff --git a/web/src/pages/dataset/compilation/knowledge-force-graph.tsx b/web/src/pages/dataset/compilation/knowledge-force-graph.tsx deleted file mode 100644 index fdb338324d..0000000000 --- a/web/src/pages/dataset/compilation/knowledge-force-graph.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import { useIsDarkTheme } from '@/components/theme-provider'; -import { cn } from '@/lib/utils'; -import isEmpty from 'lodash/isEmpty'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import ForceGraph2D, { LinkObject, NodeObject } from 'react-force-graph-2d'; - -import { buildNodesAndCombos } from '../knowledge-graph/util'; - -interface IProps { - data: any; - show: boolean; -} - -interface NodeData { - rank?: number; - entity_type?: string; - description?: string; - weight?: number; - communities?: string[]; - combo?: string; - __bckgDimensions?: [number, number]; -} - -interface LinkData { - weight?: number; -} - -type GNode = NodeObject; -type GLink = LinkObject; - -const NodeColorPalette = [ - '#5B8FF9', - '#5AD8A6', - '#F6BD16', - '#E8684A', - '#6DC8EC', - '#9270CA', - '#FF9D4D', - '#269A99', - '#FF99C3', - '#A9A9A9', -]; - -const getNodeSize = (node: GNode) => { - const size = 180 + ((node.rank as number) || 0) * 5; - return Math.min(size, 300) / 2; -}; - -const getLinkWidth = (link: GLink) => { - const weight = Number(link.weight) || 2; - return Math.min(weight * 4, 8); -}; - -export function KnowledgeForceGraph({ data, show }: IProps) { - const containerRef = useRef(null); - const fgRef = useRef(null); - const isDark = useIsDarkTheme(); - const [hoverNode, setHoverNode] = useState(null); - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); - const forceConfiguredRef = useRef(false); - - const graphData = useMemo(() => { - if (isEmpty(data)) { - return { nodes: [], links: [] }; - } - - const rawNodes = data.nodes || []; - const rawEdges = data.edges || []; - const { nodes: comboNodes } = buildNodesAndCombos(rawNodes); - - const entityTypes = Array.from( - new Set(rawNodes.map((n: any) => n.entity_type).filter(Boolean)), - ); - - const typeColorMap = entityTypes.reduce>( - (pre, cur, idx) => { - pre[cur as string] = NodeColorPalette[idx % NodeColorPalette.length]; - return pre; - }, - {}, - ); - - const nodes = comboNodes.map((n: any) => ({ - ...n, - color: typeColorMap[n.entity_type as string] || NodeColorPalette[0], - })); - - const links = rawEdges.map((e: any) => ({ - source: e.source, - target: e.target, - weight: e.weight, - })); - - return { nodes, links }; - }, [data]); - - // Compute the set of node IDs directly connected to the hovered node (degree: 1) - const connectedNodeIds = useMemo(() => { - if (!hoverNode) return new Set(); - const ids = new Set(); - ids.add(hoverNode.id as string); - graphData.links.forEach((link: any) => { - const sourceId = - typeof link.source === 'object' ? link.source.id : link.source; - const targetId = - typeof link.target === 'object' ? link.target.id : link.target; - if (sourceId === hoverNode.id) ids.add(targetId as string); - if (targetId === hoverNode.id) ids.add(sourceId as string); - }); - return ids; - }, [hoverNode, graphData.links]); - - useEffect(() => { - if (!containerRef.current || !show) return; - - const observer = new ResizeObserver((entries) => { - const entry = entries[0]; - if (entry) { - const { width, height } = entry.contentRect; - setDimensions({ width, height }); - } - }); - - observer.observe(containerRef.current); - return () => observer.disconnect(); - }, [show]); - - // Reset the flag when graphData changes, so forces are reconfigured for new data - useEffect(() => { - forceConfiguredRef.current = false; - }, [graphData]); - - useEffect(() => { - if ( - fgRef.current && - !isEmpty(graphData.nodes) && - !forceConfiguredRef.current - ) { - forceConfiguredRef.current = true; - // D3 force configuration - const linkForce = fgRef.current.d3Force('link'); - if (linkForce) { - linkForce.distance((link: GLink) => { - const source = - typeof link.source === 'object' - ? (link.source as GNode) - : (graphData.nodes.find( - (n: GNode) => n.id === link.source, - ) as GNode); - const target = - typeof link.target === 'object' - ? (link.target as GNode) - : (graphData.nodes.find( - (n: GNode) => n.id === link.target, - ) as GNode); - const sourceSize = source ? getNodeSize(source) : 16; - const targetSize = target ? getNodeSize(target) : 16; - return sourceSize + targetSize + 200; - }); - } - - const chargeForce = fgRef.current.d3Force('charge'); - if (chargeForce) { - chargeForce.strength(-1000); - } - - const timer = setTimeout(() => { - fgRef.current?.zoomToFit(400, 50); - }, 600); - return () => clearTimeout(timer); - } - }, [graphData, dimensions]); - - const getNodeLabel = useCallback((node: GNode) => { - const parts = [`

${node.id}

`]; - - if (node.entity_type) { - parts.push( - `
Entity type:
${node.entity_type}
`, - ); - } - - if (node.weight) { - parts.push( - `
Weight:
${node.weight}
`, - ); - } - - if (node.description) { - parts.push( - `

${node.description}

`, - ); - } - - return `
${parts.join('')}
`; - }, []); - - const getLinkLabel = useCallback((link: GLink) => { - const sourceId = - typeof link.source === 'object' ? link.source.id : link.source; - const targetId = - typeof link.target === 'object' ? link.target.id : link.target; - return `${sourceId} → ${targetId}`; - }, []); - - const nodeCanvasObject = useCallback( - (node: GNode, ctx: CanvasRenderingContext2D, globalScale: number) => { - const size = getNodeSize(node); - const label = (node.id as string) || ''; - const fontSize = Math.max(12 / globalScale, Math.min(size / 2, 40)); - - // Dim non-connected nodes when hovering - const isDimmed = hoverNode && !connectedNodeIds.has(node.id as string); - ctx.globalAlpha = isDimmed ? 0.1 : 1; - - // Node circle (solid fill by entity_type color) - const nodeColor = (node as any).color || NodeColorPalette[0]; - ctx.beginPath(); - ctx.arc(node.x!, node.y!, size, 0, 2 * Math.PI); - ctx.fillStyle = nodeColor; - ctx.fill(); - ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)'; - ctx.lineWidth = Math.max(2 / globalScale, 3); - ctx.stroke(); - - // Only show label on hover - if (hoverNode?.id === node.id) { - ctx.font = `${fontSize}px Sans-Serif`; - const textWidth = ctx.measureText(label).width; - const padding = fontSize * 0.4; - const bckgDimensions: [number, number] = [ - textWidth + padding * 2, - fontSize + padding * 2, - ]; - - // Label background - ctx.fillStyle = isDark - ? 'rgba(0, 0, 0, 0.1)' - : 'rgba(255, 255, 255, 0.1)'; - ctx.fillRect( - node.x! - bckgDimensions[0] / 2, - node.y! + size + fontSize / 2, - bckgDimensions[0], - bckgDimensions[1], - ); - - // Label text - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = isDark ? '#fff' : '#000'; - ctx.fillText(label, node.x!, node.y! + size + fontSize); - - (node as any).__bckgDimensions = bckgDimensions; - } - - ctx.globalAlpha = 1; - }, - [isDark, hoverNode, connectedNodeIds], - ); - - const nodePointerAreaPaint = useCallback( - (node: GNode, color: string, ctx: CanvasRenderingContext2D) => { - const size = getNodeSize(node); - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(node.x!, node.y!, size, 0, 2 * Math.PI); - ctx.fill(); - }, - [], - ); - - const linkCanvasObject = useCallback( - (link: GLink, ctx: CanvasRenderingContext2D) => { - const source = - typeof link.source === 'object' - ? (link.source as GNode) - : (graphData.nodes.find((n: GNode) => n.id === link.source) as GNode); - const target = - typeof link.target === 'object' - ? (link.target as GNode) - : (graphData.nodes.find((n: GNode) => n.id === link.target) as GNode); - - if (!source || !target) return; - - const isDimmed = - hoverNode && - !( - connectedNodeIds.has(source.id as string) && - connectedNodeIds.has(target.id as string) - ); - ctx.globalAlpha = isDimmed ? 0.1 : 1; - - const lineWidth = getLinkWidth(link); - ctx.lineWidth = lineWidth; - ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'; - - ctx.beginPath(); - ctx.moveTo(source.x!, source.y!); - ctx.lineTo(target.x!, target.y!); - ctx.stroke(); - ctx.globalAlpha = 1; - }, - [graphData.nodes, hoverNode, connectedNodeIds, isDark], - ); - - const handleNodeHover = useCallback((node: GNode | null) => { - setHoverNode(node); - }, []); - - const backgroundColor = isDark ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)'; - - return ( -
- {dimensions.width > 0 && dimensions.height > 0 && ( - - isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)' - } - onNodeHover={handleNodeHover} - enableNodeDrag - enableZoomInteraction - enablePanInteraction - warmupTicks={50} - cooldownTicks={100} - /> - )} -
- ); -} - -export default KnowledgeForceGraph; diff --git a/web/src/pages/dataset/compilation/llm-wiki-view.tsx b/web/src/pages/dataset/compilation/llm-wiki-view.tsx new file mode 100644 index 0000000000..55f7589c1f --- /dev/null +++ b/web/src/pages/dataset/compilation/llm-wiki-view.tsx @@ -0,0 +1,104 @@ +import { Card } from '@/components/ui/card'; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from '@/components/ui/resizable'; +import { + ArtifactKeys, + ArtifactTopicKeys, + useFetchArtifactTopicList, + 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 { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'react-router'; + +import { LeftPanelTab } from './constants'; +import CompilationEmptyState from './empty-state'; +import { useCompilationArtifact } from './hooks/use-compilation-artifact'; +import { CompilationLoadingCard } from './loading-card'; +import { WikiDetailContent } from './wiki-detail-content'; +import { WikiLeftPanel } from './wiki-left-panel'; + +export function LlmWikiView() { + const { id } = useParams(); + const queryClient = useQueryClient(); + const [leftTab, setLeftTab] = useState(LeftPanelTab.Contents); + const { data: knowledgeBase } = useFetchKnowledgeBaseConfiguration(); + const { topics, loading: topicListLoading } = useFetchArtifactTopicList(); + const { + selectedArtifact, + selectedVersion, + selectVersion, + handleSelectArtifact, + clearSelectedArtifact, + } = useCompilationArtifact(); + + const { data: artifactRunData } = useTraceRunData(GenerateType.Artifact); + const { status: artifactStatus } = useGenerateStatus(artifactRunData); + + useEffect(() => { + if (artifactStatus === GenerateStatus.completed) { + queryClient.invalidateQueries({ + queryKey: ArtifactKeys.listByDataset(id!), + }); + queryClient.invalidateQueries({ + queryKey: ArtifactTopicKeys.listByDataset(id!), + }); + } + }, [artifactStatus, queryClient, id]); + + const handleLeftTabChange = useCallback((value: string) => { + setLeftTab(value as LeftPanelTab); + }, []); + + const canGenerate = (knowledgeBase?.chunk_count ?? 0) > 0; + const isLoading = topicListLoading && topics.length === 0; + const isEmpty = topics.length === 0 && !topicListLoading; + + if (isLoading) { + return ; + } + + if (isEmpty) { + return ( + + ); + } + + return ( + + + + + + + + + + + + ); +} diff --git a/web/src/pages/dataset/compilation/loading-card.tsx b/web/src/pages/dataset/compilation/loading-card.tsx new file mode 100644 index 0000000000..cfd51c301a --- /dev/null +++ b/web/src/pages/dataset/compilation/loading-card.tsx @@ -0,0 +1,10 @@ +import { SkeletonCard } from '@/components/skeleton-card'; +import { Card } from '@/components/ui/card'; + +export function CompilationLoadingCard() { + return ( + + + + ); +} diff --git a/web/src/pages/dataset/compilation/skills-left-panel.tsx b/web/src/pages/dataset/compilation/skills-left-panel.tsx index 3e1ce489f1..0d0b65885f 100644 --- a/web/src/pages/dataset/compilation/skills-left-panel.tsx +++ b/web/src/pages/dataset/compilation/skills-left-panel.tsx @@ -1,61 +1,79 @@ +import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { Button } from '@/components/ui/button'; import { SearchInput } from '@/components/ui/input'; import { Spin } from '@/components/ui/spin'; -import { useFetchDatasetSkillTree } from '@/hooks/use-dataset-skill-request'; -import { DatasetSkillTreeNode } from '@/interfaces/database/dataset-skill'; -import { cn } from '@/lib/utils'; +import { TreeDataItem, TreeView } from '@/components/ui/tree-view'; +import { + useDeleteDatasetSkillPage, + useDeleteDatasetSkillTree, + useFetchDatasetSkillTree, +} from '@/hooks/use-dataset-skill-request'; import { useDebounce } from 'ahooks'; +import { FileText, Folder, Trash2 } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { + buildSkillTreeData, + countSkillTreeNodes, + filterSkillTreeData, +} from './utils/skill-tree'; -type FlatSkillNode = { - skill_kwd: string; - depth: number; -}; +// TreeView only computes expandedItemIds when initialSelectedItemId is +// truthy; combined with expandAll, any truthy id makes every branch mount +// open. A sentinel that matches no real skill_kwd forces expand-all without +// highlighting any row as selected. +const ExpandAllSentinelId = '__skill-tree-expand-all-sentinel__'; type SkillsLeftPanelProps = { selectedSkill: string | null; - onSelectSkill: (skillKwd: string) => void; + onSelectSkill: (skillKwd: string | null) => void; }; -function flattenSkillTree(nodes: DatasetSkillTreeNode[] = []): FlatSkillNode[] { - const result: FlatSkillNode[] = []; - - function walk(items: DatasetSkillTreeNode[], depth: number) { - for (const item of items) { - result.push({ skill_kwd: item.skill_kwd, depth }); - if (item.children_kwd?.length) { - walk(item.children_kwd, depth + 1); - } - } - } - - walk(nodes, 0); - return result; -} - -type SkillListItemProps = { - item: FlatSkillNode; - isSelected: boolean; - onSelect: (skillKwd: string) => void; +type SkillDeleteActionProps = { + skillKwd: string; + deleteLoading: boolean; + onDelete: (skillKwd: string) => void; }; -function SkillListItem({ item, isSelected, onSelect }: SkillListItemProps) { - const handleClick = useCallback(() => { - onSelect(item.skill_kwd); - }, [item.skill_kwd, onSelect]); +function SkillDeleteAction({ + skillKwd, + deleteLoading, + onDelete, +}: SkillDeleteActionProps) { + const { t } = useTranslation(); + + const handleTriggerClick = useCallback( + (e: React.MouseEvent) => { + // TreeView does not guard action clicks: without this the row would + // also get selected and a branch row would toggle its accordion. + e.stopPropagation(); + }, + [], + ); + + const handleConfirmDelete = useCallback(() => { + onDelete(skillKwd); + }, [skillKwd, onDelete]); return ( -
  • - {item.skill_kwd} -
  • + + ); } @@ -65,22 +83,18 @@ export function SkillsLeftPanel({ }: SkillsLeftPanelProps) { const { t } = useTranslation(); const { data: tree, loading } = useFetchDatasetSkillTree(); + const { deleteSkillTree, loading: deleteTreeLoading } = + useDeleteDatasetSkillTree(); + const { deleteSkillPage, loading: deletePageLoading } = + useDeleteDatasetSkillPage(); const [searchString, setSearchString] = useState(''); const debouncedSearchString = useDebounce(searchString, { wait: 500 }); - const allNodes = useMemo( - () => flattenSkillTree(tree?.skill_with_weight), + const totalCount = useMemo( + () => countSkillTreeNodes(tree?.skill_with_weight), [tree?.skill_with_weight], ); - const filteredNodes = useMemo(() => { - const keyword = debouncedSearchString.trim().toLowerCase(); - if (!keyword) return allNodes; - return allNodes.filter((node) => - node.skill_kwd.toLowerCase().includes(keyword), - ); - }, [allNodes, debouncedSearchString]); - const handleSearchChange = useCallback( (e: React.ChangeEvent) => { setSearchString(e.target.value); @@ -88,8 +102,73 @@ export function SkillsLeftPanel({ [], ); + const handleDeleteAll = useCallback(async () => { + const data = await deleteSkillTree(); + if (data?.code === 0) { + onSelectSkill(null); + } + }, [deleteSkillTree, onSelectSkill]); + + const handleDeleteSkill = useCallback( + async (skillKwd: string) => { + const data = await deleteSkillPage(skillKwd); + if (data?.code === 0 && selectedSkill === skillKwd) { + onSelectSkill(null); + } + }, + [deleteSkillPage, selectedSkill, onSelectSkill], + ); + + const renderSkillActions = useCallback( + (skillKwd: string) => ( + + ), + [deletePageLoading, handleDeleteSkill], + ); + + const treeData = useMemo( + () => buildSkillTreeData(tree?.skill_with_weight, renderSkillActions), + [tree?.skill_with_weight, renderSkillActions], + ); + + const filteredTreeData = useMemo( + () => filterSkillTreeData(treeData, debouncedSearchString), + [treeData, debouncedSearchString], + ); + + const handleTreeSelect = useCallback( + (item: TreeDataItem | undefined) => { + onSelectSkill(item?.id ?? null); + }, + [onSelectSkill], + ); + return ( 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({