mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-27 10:52:03 +08:00
Feat: Render the skills list using a tree view. (#17115)
### Summary Feat: Render the skills list using a tree view.
This commit is contained in:
@@ -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({
|
||||
<ParseTypeItem />
|
||||
{parseType === ParseType.BuiltIn && <ChunkMethodItem />}
|
||||
|
||||
{parseType === ParseType.BuiltIn && (
|
||||
<CompilationTemplateFormField></CompilationTemplateFormField>
|
||||
)}
|
||||
|
||||
{showPages && parseType === ParseType.BuiltIn && (
|
||||
<DynamicPageRange />
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,6 @@ export function useDefaultParserValues() {
|
||||
metadata: [],
|
||||
built_in_metadata: [],
|
||||
enable_metadata: false,
|
||||
compilation_template_group_id: [],
|
||||
};
|
||||
|
||||
return defaultParserValues as IParserConfig;
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<string>();
|
||||
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<MultiSelectOptionType[]>(() => {
|
||||
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 ? (
|
||||
<span className="text-text-secondary ml-1">
|
||||
({t(`knowledgeConfiguration.${scopeTranslationKey}`)})
|
||||
</span>
|
||||
) : undefined,
|
||||
};
|
||||
});
|
||||
}, [groups, selectedScopes, value, t]);
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
options={options}
|
||||
onValueChange={onChange}
|
||||
defaultValue={value}
|
||||
value={value}
|
||||
showSelectAll={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<RAGFlowFormItem
|
||||
@@ -107,9 +29,10 @@ export function CompilationTemplateFormField({
|
||||
horizontal={horizontal}
|
||||
>
|
||||
{(field) => (
|
||||
<CompilationTemplateMultiSelect
|
||||
value={field.value ?? []}
|
||||
<SelectWithSearch
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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: '注册成功',
|
||||
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<Inspector keys={['alt', 'c']} onInspectElement={gotoVSCode} />
|
||||
<Inspector keys={['alt', 'c']} />
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function DocumentViewSwitch({
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
<LayoutList className="h-4 w-4" />
|
||||
<span>{t('chunk.representation', 'Representation')}</span>
|
||||
<span>Artifact</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum ViewMode {
|
||||
Graph = 'graph',
|
||||
LlmWiki = 'llm-wiki',
|
||||
Skills = 'skills',
|
||||
}
|
||||
|
||||
@@ -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>(LeftPanelTab.Contents);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(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,
|
||||
};
|
||||
}
|
||||
@@ -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>(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 = (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col p-8">
|
||||
<SkeletonCard className="flex-1" />
|
||||
</Card>
|
||||
);
|
||||
const handleSwitchToSkills = useCallback(() => {
|
||||
setViewMode(ViewMode.Skills);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col p-4 gap-4 h-full">
|
||||
@@ -112,80 +59,13 @@ export default function Compilation() {
|
||||
size="sm"
|
||||
onClick={handleSwitchToSkills}
|
||||
>
|
||||
{t('knowledgeDetails.skills')}
|
||||
To Skills
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
{viewMode === ViewMode.Graph ? (
|
||||
isGraphLoading ? (
|
||||
loadingCard
|
||||
) : (
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<KnowledgeForceGraph data={knowledgeGraph?.graph} show />
|
||||
</div>
|
||||
)
|
||||
) : viewMode === ViewMode.LlmWiki ? (
|
||||
isLlmWikiLoading ? (
|
||||
loadingCard
|
||||
) : isLlmWikiEmpty ? (
|
||||
<CompilationEmptyState
|
||||
type="llm-wiki"
|
||||
disabled={!canGenerate}
|
||||
data={artifactRunData}
|
||||
/>
|
||||
) : (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1">
|
||||
<ResizablePanel defaultSize={33} minSize={20} maxSize={50}>
|
||||
<WikiLeftPanel
|
||||
tab={leftTab}
|
||||
onTabChange={handleLeftTabChange}
|
||||
selectedArtifact={selectedArtifact}
|
||||
onSelectArtifact={handleSelectArtifact}
|
||||
onClearWiki={clearSelectedArtifact}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel>
|
||||
<WikiDetailContent
|
||||
selectedArtifact={selectedArtifact}
|
||||
selectedVersion={selectedVersion}
|
||||
onSelectVersion={selectVersion}
|
||||
onSelectArtifact={handleSelectArtifact}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</Card>
|
||||
)
|
||||
) : isSkillsLoading ? (
|
||||
loadingCard
|
||||
) : isSkillsEmpty ? (
|
||||
<CompilationEmptyState
|
||||
type="skills"
|
||||
disabled={!canGenerate}
|
||||
data={skillRunData}
|
||||
/>
|
||||
) : (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1">
|
||||
<ResizablePanel defaultSize={33} minSize={20} maxSize={50}>
|
||||
<SkillsLeftPanel
|
||||
selectedSkill={selectedSkill}
|
||||
onSelectSkill={setSelectedSkill}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel className="flex flex-col">
|
||||
<MarkdownEditor
|
||||
content={skillPage?.md_with_weight ?? ''}
|
||||
readOnly
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</Card>
|
||||
)}
|
||||
{viewMode === ViewMode.LlmWiki ? <LlmWikiView /> : <SkillsView />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<NodeData>;
|
||||
type GLink = LinkObject<NodeData, LinkData>;
|
||||
|
||||
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<HTMLDivElement>(null);
|
||||
const fgRef = useRef<any>(null);
|
||||
const isDark = useIsDarkTheme();
|
||||
const [hoverNode, setHoverNode] = useState<GNode | null>(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<Record<string, string>>(
|
||||
(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<string>();
|
||||
const ids = new Set<string>();
|
||||
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 = [`<h3 class="font-semibold">${node.id}</h3>`];
|
||||
|
||||
if (node.entity_type) {
|
||||
parts.push(
|
||||
`<div class="flex items-center gap-[.5ch]"><dt><b>Entity type: </b></dt><dd>${node.entity_type}</dd></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
if (node.weight) {
|
||||
parts.push(
|
||||
`<div class="flex items-center gap-[.5ch]"><dt><b>Weight: </b></dt><dd>${node.weight}</dd></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
if (node.description) {
|
||||
parts.push(
|
||||
`<p class="text-xs mt-1 max-w-[240px]">${node.description}</p>`,
|
||||
);
|
||||
}
|
||||
|
||||
return `<dl class="mb-1 empty:hidden">${parts.join('')}</dl>`;
|
||||
}, []);
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('flex-1 min-h-0', !show && 'hidden')}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{dimensions.width > 0 && dimensions.height > 0 && (
|
||||
<ForceGraph2D
|
||||
ref={fgRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
graphData={graphData}
|
||||
backgroundColor={backgroundColor}
|
||||
nodeLabel={getNodeLabel}
|
||||
linkLabel={getLinkLabel}
|
||||
nodeRelSize={1}
|
||||
nodeCanvasObject={nodeCanvasObject}
|
||||
nodePointerAreaPaint={nodePointerAreaPaint}
|
||||
linkCanvasObject={linkCanvasObject}
|
||||
linkWidth={getLinkWidth}
|
||||
linkColor={() =>
|
||||
isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
|
||||
}
|
||||
onNodeHover={handleNodeHover}
|
||||
enableNodeDrag
|
||||
enableZoomInteraction
|
||||
enablePanInteraction
|
||||
warmupTicks={50}
|
||||
cooldownTicks={100}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KnowledgeForceGraph;
|
||||
104
web/src/pages/dataset/compilation/llm-wiki-view.tsx
Normal file
104
web/src/pages/dataset/compilation/llm-wiki-view.tsx
Normal file
@@ -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>(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 <CompilationLoadingCard />;
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<CompilationEmptyState
|
||||
type="llm-wiki"
|
||||
disabled={!canGenerate}
|
||||
data={artifactRunData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1">
|
||||
<ResizablePanel defaultSize={33} minSize={20} maxSize={50}>
|
||||
<WikiLeftPanel
|
||||
tab={leftTab}
|
||||
onTabChange={handleLeftTabChange}
|
||||
selectedArtifact={selectedArtifact}
|
||||
onSelectArtifact={handleSelectArtifact}
|
||||
onClearWiki={clearSelectedArtifact}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel>
|
||||
<WikiDetailContent
|
||||
selectedArtifact={selectedArtifact}
|
||||
selectedVersion={selectedVersion}
|
||||
onSelectVersion={selectVersion}
|
||||
onSelectArtifact={handleSelectArtifact}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
10
web/src/pages/dataset/compilation/loading-card.tsx
Normal file
10
web/src/pages/dataset/compilation/loading-card.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SkeletonCard } from '@/components/skeleton-card';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function CompilationLoadingCard() {
|
||||
return (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col p-8">
|
||||
<SkeletonCard className="flex-1" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
// 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 (
|
||||
<li
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-md text-sm cursor-pointer',
|
||||
'text-text-secondary hover:bg-bg-base hover:text-text-primary',
|
||||
isSelected && 'bg-bg-card text-text-primary',
|
||||
)}
|
||||
style={{ paddingLeft: `${item.depth * 16 + 12}px` }}
|
||||
<ConfirmDeleteDialog
|
||||
title={t('datasetSkill.deleteSkillTitle')}
|
||||
content={{ title: t('datasetSkill.deleteSkillDescription') }}
|
||||
onOk={handleConfirmDelete}
|
||||
>
|
||||
<span className="truncate">{item.skill_kwd}</span>
|
||||
</li>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={deleteLoading}
|
||||
onClick={handleTriggerClick}
|
||||
// TreeActions keeps actions always visible on the selected row;
|
||||
// hide again so the button only appears while hovering the row.
|
||||
// `hidden` (not opacity-0) so no invisible click target remains.
|
||||
className="hidden group-hover:inline-flex"
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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) => (
|
||||
<SkillDeleteAction
|
||||
skillKwd={skillKwd}
|
||||
deleteLoading={deletePageLoading}
|
||||
onDelete={handleDeleteSkill}
|
||||
/>
|
||||
),
|
||||
[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 (
|
||||
<aside className="size-full flex flex-col">
|
||||
<section className="flex items-center justify-between px-3 pt-3">
|
||||
<span className="text-sm font-medium text-text-primary">
|
||||
{t('datasetSkill.folders')} ({totalCount})
|
||||
</span>
|
||||
<ConfirmDeleteDialog
|
||||
title={t('datasetSkill.deleteAllTitle')}
|
||||
content={{ title: t('datasetSkill.deleteAllDescription') }}
|
||||
onOk={handleDeleteAll}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={deleteTreeLoading}
|
||||
data-testid="skills-clear-trigger"
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</section>
|
||||
|
||||
<div className="px-3 py-2">
|
||||
<SearchInput
|
||||
placeholder={t('common.search')}
|
||||
@@ -98,28 +177,26 @@ export function SkillsLeftPanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-3 pb-3">
|
||||
{loading && filteredNodes.length === 0 ? (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-1 pb-3">
|
||||
{loading && filteredTreeData.length === 0 ? (
|
||||
<div className="py-8 flex justify-center">
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : filteredNodes.length === 0 ? (
|
||||
) : filteredTreeData.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-text-secondary">
|
||||
{debouncedSearchString
|
||||
? t('common.noData')
|
||||
: t('datasetSkill.empty')}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{filteredNodes.map((item) => (
|
||||
<SkillListItem
|
||||
key={item.skill_kwd}
|
||||
item={item}
|
||||
isSelected={selectedSkill === item.skill_kwd}
|
||||
onSelect={onSelectSkill}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<TreeView
|
||||
data={filteredTreeData}
|
||||
initialSelectedItemId={ExpandAllSentinelId}
|
||||
onSelectChange={handleTreeSelect}
|
||||
expandAll
|
||||
defaultNodeIcon={Folder}
|
||||
defaultLeafIcon={FileText}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
82
web/src/pages/dataset/compilation/skills-view.tsx
Normal file
82
web/src/pages/dataset/compilation/skills-view.tsx
Normal file
@@ -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 <CompilationLoadingCard />;
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<CompilationEmptyState
|
||||
type="skills"
|
||||
disabled={!canGenerate}
|
||||
data={skillRunData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1">
|
||||
<ResizablePanel defaultSize={33} minSize={20} maxSize={50}>
|
||||
<SkillsLeftPanel
|
||||
selectedSkill={selectedSkill}
|
||||
onSelectSkill={setSelectedSkill}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel className="flex flex-col">
|
||||
<MarkdownEditor content={skillPage?.md_with_weight ?? ''} readOnly />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
65
web/src/pages/dataset/compilation/utils/skill-tree.ts
Normal file
65
web/src/pages/dataset/compilation/utils/skill-tree.ts
Normal file
@@ -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<TreeDataItem[]>((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,
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<CompilationTemplateFormField horizontal></CompilationTemplateFormField>
|
||||
<LayoutRecognizeFormField
|
||||
testId="ds-settings-parser-pdf-parser-select"
|
||||
ownerTenantId={ownerTenantId}
|
||||
|
||||
@@ -92,7 +92,6 @@ export const formSchema = z
|
||||
path: ['entity_types'],
|
||||
},
|
||||
),
|
||||
compilation_template_group_id: z.array(z.string()).optional(),
|
||||
metadata: z.any().optional(),
|
||||
built_in_metadata: z
|
||||
.array(
|
||||
|
||||
@@ -105,7 +105,6 @@ export default function DatasetSettings() {
|
||||
additionalProperties: false,
|
||||
},
|
||||
built_in_metadata: [],
|
||||
compilation_template_group_id: [],
|
||||
enable_metadata: false,
|
||||
llm_id: '',
|
||||
},
|
||||
|
||||
@@ -69,40 +69,16 @@ const useTraceQuery = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useTraceGenerate = ({ open }: { open: boolean }) => {
|
||||
const TraceTypeMap: Record<GenerateType, TraceType> = {
|
||||
[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({
|
||||
|
||||
@@ -68,7 +68,7 @@ export function SideBar({ dataset: data }: PropType) {
|
||||
: []),
|
||||
{
|
||||
icon: <LucideBookText className="size-[1em]" />,
|
||||
label: t(`knowledgeDetails.compilation`),
|
||||
label: 'Artifacts',
|
||||
key: Routes.Compilation,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -96,6 +96,7 @@ export function AddFieldModal({
|
||||
handleTypeChange(value);
|
||||
}}
|
||||
placeholder={t('setting.selectFieldType')}
|
||||
allowCustomValue
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
@@ -105,7 +106,8 @@ export function AddFieldModal({
|
||||
<RAGFlowFormItem key={key} name={key} label={getFieldLabel(key)}>
|
||||
<Textarea
|
||||
placeholder={t('setting.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
rows={key === 'description' ? 4 : 10}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
))}
|
||||
|
||||
@@ -23,7 +23,11 @@ export function BasicInfoStep() {
|
||||
label={t('setting.groupDescription')}
|
||||
horizontal
|
||||
>
|
||||
<Textarea placeholder={t('common.descriptionPlaceholder')} rows={3} />
|
||||
<Textarea
|
||||
placeholder={t('common.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<RAGFlowFormItem name="avatar" label={t('setting.avatar')} horizontal>
|
||||
|
||||
@@ -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<FormSchemaType>;
|
||||
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<FormSchemaType>();
|
||||
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<string, string>[] | 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 (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{fields.map((field, index) => {
|
||||
@@ -78,27 +48,25 @@ export function SectionFieldGrid({
|
||||
);
|
||||
})}
|
||||
|
||||
{!allTypesUsed && (
|
||||
<Card
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onOpenAddField}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onOpenAddField();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'border-border-button bg-transparent border-dashed flex flex-col items-center justify-center gap-2 min-h-[140px] cursor-pointer',
|
||||
'hover:border-border-accent hover:text-text-primary text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center gap-2 p-4">
|
||||
<Plus className="size-6" />
|
||||
<span className="text-sm font-medium">{t('setting.addField')}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Card
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onOpenAddField}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onOpenAddField();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'border-border-button bg-transparent border-dashed flex flex-col items-center justify-center gap-2 min-h-[140px] cursor-pointer',
|
||||
'hover:border-border-accent hover:text-text-primary text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center gap-2 p-4">
|
||||
<Plus className="size-6" />
|
||||
<span className="text-sm font-medium">{t('setting.addField')}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<RAGFlowFormItem
|
||||
name={`templates.${selectedTemplateIndex}.name`}
|
||||
label={t('setting.templateName')}
|
||||
required
|
||||
>
|
||||
<Input placeholder={t('common.namePlaceholder')} />
|
||||
</RAGFlowFormItem>
|
||||
@@ -158,6 +157,7 @@ export function TemplateConfiguration({
|
||||
<Textarea
|
||||
placeholder={t('common.descriptionPlaceholder')}
|
||||
rows={2}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
@@ -189,7 +189,8 @@ export function TemplateConfiguration({
|
||||
>
|
||||
<Textarea
|
||||
placeholder={t('setting.globalRulesPlaceholder')}
|
||||
rows={4}
|
||||
rows={8}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user