mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-09 00:47:59 +08:00
Feat: Search for timeline nodes. (#17996)
This commit is contained in:
@@ -153,37 +153,6 @@ export function adaptTreeToTreeData(
|
||||
return buildTreeDataItems(template.entities, template.relations, ['child']);
|
||||
}
|
||||
|
||||
function filterTreeDataItems(
|
||||
items: TreeDataItem[],
|
||||
keyword: string,
|
||||
): TreeDataItem[] {
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
|
||||
return items.reduce<TreeDataItem[]>((acc, item) => {
|
||||
const children = item.children
|
||||
? filterTreeDataItems(item.children, keyword)
|
||||
: [];
|
||||
const matches = item.name.toLowerCase().includes(lowerKeyword);
|
||||
|
||||
if (matches || children.length > 0) {
|
||||
acc.push({
|
||||
...item,
|
||||
children: children.length > 0 ? children : item.children,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function filterTreeDataByKeyword(
|
||||
data: TreeDataItem[],
|
||||
keyword: string,
|
||||
): TreeDataItem[] {
|
||||
if (!keyword.trim()) return data;
|
||||
return filterTreeDataItems(data, keyword);
|
||||
}
|
||||
|
||||
export function adaptKnowledgeGraphToForceGraph(
|
||||
template: IStructureGraphTemplate,
|
||||
): IArtifactGraph {
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
adaptPageIndexToTreeData,
|
||||
adaptTimelineToX6Data,
|
||||
adaptTreeToTreeData,
|
||||
filterTreeDataByKeyword,
|
||||
} from './adapters';
|
||||
import MindMapG6Graph from './mindmap-g6-graph';
|
||||
import TimelineX6Graph from './timeline-x6-graph';
|
||||
@@ -31,7 +30,6 @@ const EmptyForceGraphData: IArtifactGraph = { entities: [], relations: [] };
|
||||
|
||||
interface RepresentationRendererProps {
|
||||
template?: IStructureGraphTemplate;
|
||||
searchKeyword?: string;
|
||||
onNodeClick?: (node: ClickableNode) => void;
|
||||
highlightNodeId?: string | null;
|
||||
}
|
||||
@@ -51,7 +49,6 @@ function UnsupportedPlaceholder({ kind }: { kind: StructureTemplateKind }) {
|
||||
|
||||
export function RepresentationRenderer({
|
||||
template,
|
||||
searchKeyword = '',
|
||||
onNodeClick,
|
||||
highlightNodeId,
|
||||
}: RepresentationRendererProps) {
|
||||
@@ -104,22 +101,16 @@ export function RepresentationRenderer({
|
||||
[],
|
||||
);
|
||||
|
||||
const filteredTreeData = useMemo<TreeDataItem[]>(() => {
|
||||
const treeData = useMemo<TreeDataItem[]>(() => {
|
||||
if (!template) return [];
|
||||
if (template.kind === CompilationTemplateKind.PageIndex) {
|
||||
const data = adaptPageIndexToTreeData(template);
|
||||
return searchKeyword.trim()
|
||||
? filterTreeDataByKeyword(data, searchKeyword)
|
||||
: data;
|
||||
return adaptPageIndexToTreeData(template);
|
||||
}
|
||||
if (template.kind === CompilationTemplateKind.Tree) {
|
||||
const data = adaptTreeToTreeData(template);
|
||||
return searchKeyword.trim()
|
||||
? filterTreeDataByKeyword(data, searchKeyword)
|
||||
: data;
|
||||
return adaptTreeToTreeData(template);
|
||||
}
|
||||
return [];
|
||||
}, [template, searchKeyword]);
|
||||
}, [template]);
|
||||
|
||||
// Keep a stable reference across re-renders so the memoized ArtifactForceGraph
|
||||
// does not restart its force simulation when only highlightNodeId changes
|
||||
@@ -140,7 +131,7 @@ export function RepresentationRenderer({
|
||||
return (
|
||||
<div className="mt-6 overflow-auto scrollbar-auto">
|
||||
<TreeView
|
||||
data={filteredTreeData}
|
||||
data={treeData}
|
||||
expandAll
|
||||
onSelectChange={handleTreeItemClick}
|
||||
/>
|
||||
@@ -150,7 +141,7 @@ export function RepresentationRenderer({
|
||||
return (
|
||||
<div className="mt-6 overflow-auto scrollbar-auto">
|
||||
<TreeView
|
||||
data={filteredTreeData}
|
||||
data={treeData}
|
||||
expandAll
|
||||
onSelectChange={handleTreeItemClick}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getEntityDisplayName } from '@/components/structure-graph/adapters';
|
||||
import { type ClickableNode } from '@/components/structure-graph/representation-renderer';
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
import { useFetchDocumentStructureGraph } from '@/hooks/use-document-request';
|
||||
import { useDebounce } from 'ahooks';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelectedTemplate } from './use-selected-template';
|
||||
|
||||
@@ -10,9 +11,16 @@ export function useGraphEntitySearch(
|
||||
onNodeClick?: (node: ClickableNode) => void,
|
||||
) {
|
||||
const [graphKeywords, setGraphKeywords] = useState('');
|
||||
const [searchKeyword, setSearchKeyword] = useState(''); // ExpandableSearchInput value
|
||||
const [selectedNodeId, setSelectedNodeId] = useState(''); // entity name
|
||||
|
||||
const { data, loading } = useFetchDocumentStructureGraph(graphKeywords);
|
||||
// Non-graph kinds search server-side with the same ?keywords= query the
|
||||
// graph-kind Enter search uses; the two inputs never show at once, and the
|
||||
// handlers below keep at most one of the two keyword sources non-empty.
|
||||
const debouncedSearchKeyword = useDebounce(searchKeyword, { wait: 500 });
|
||||
const { data, loading } = useFetchDocumentStructureGraph(
|
||||
debouncedSearchKeyword || graphKeywords,
|
||||
);
|
||||
const templates = useMemo(() => data?.templates ?? [], [data?.templates]);
|
||||
const {
|
||||
selectedTemplateId,
|
||||
@@ -49,6 +57,7 @@ export function useGraphEntitySearch(
|
||||
(name: string) => {
|
||||
if (!name) {
|
||||
setGraphKeywords('');
|
||||
setSearchKeyword('');
|
||||
setSelectedNodeId('');
|
||||
return;
|
||||
}
|
||||
@@ -69,9 +78,14 @@ export function useGraphEntitySearch(
|
||||
|
||||
const handleNoMatchEnter = useCallback((keywords: string) => {
|
||||
setGraphKeywords(keywords);
|
||||
setSearchKeyword('');
|
||||
setSelectedNodeId('');
|
||||
}, []);
|
||||
|
||||
const handleSearchKeywordChange = useCallback((value: string) => {
|
||||
setSearchKeyword(value);
|
||||
}, []);
|
||||
|
||||
// Two-way binding: clicking a graph node selects it in the dropdown,
|
||||
// then forwards to chunk navigation like before
|
||||
const handleNodeClick = useCallback(
|
||||
@@ -89,6 +103,7 @@ export function useGraphEntitySearch(
|
||||
(templateId: string) => {
|
||||
setSelectedTemplateId(templateId);
|
||||
setGraphKeywords('');
|
||||
setSearchKeyword('');
|
||||
setSelectedNodeId('');
|
||||
},
|
||||
[setSelectedTemplateId],
|
||||
@@ -102,10 +117,12 @@ export function useGraphEntitySearch(
|
||||
selectedTemplate,
|
||||
isGraphKind,
|
||||
entityOptions,
|
||||
searchKeyword,
|
||||
graphSelectValue: selectedEntityName || graphKeywords,
|
||||
highlightNodeId: selectedEntityName || null,
|
||||
handleSelectEntity,
|
||||
handleNoMatchEnter,
|
||||
handleSearchKeywordChange,
|
||||
handleTemplateChange,
|
||||
handleNodeClick,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,14 @@ export function useSelectedTemplate(templates: IStructureGraphTemplate[]) {
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('');
|
||||
|
||||
const effectiveSelectedTemplateId = useMemo(() => {
|
||||
return selectedTemplateId || templates[0]?.template_id || '';
|
||||
// Keywords search returns a single bucket that may not be the selected
|
||||
// template — fall back to the first template instead of rendering nothing.
|
||||
if (
|
||||
templates.some((template) => template.template_id === selectedTemplateId)
|
||||
) {
|
||||
return selectedTemplateId;
|
||||
}
|
||||
return templates[0]?.template_id || '';
|
||||
}, [selectedTemplateId, templates]);
|
||||
|
||||
const selectedTemplate = useMemo(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SkeletonCard } from '@/components/skeleton-card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useDeleteDocumentStructureGraph } from '@/hooks/use-document-request';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
type ClickableNode,
|
||||
@@ -22,7 +22,6 @@ function Representation({ onNodeClick }: RepresentationProps) {
|
||||
const { t } = useTranslation();
|
||||
const { deleteDocumentStructureGraph, loading: deleting } =
|
||||
useDeleteDocumentStructureGraph();
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -32,18 +31,16 @@ function Representation({ onNodeClick }: RepresentationProps) {
|
||||
selectedTemplate,
|
||||
isGraphKind,
|
||||
entityOptions,
|
||||
searchKeyword,
|
||||
graphSelectValue,
|
||||
highlightNodeId,
|
||||
handleSelectEntity,
|
||||
handleNoMatchEnter,
|
||||
handleSearchKeywordChange,
|
||||
handleTemplateChange,
|
||||
handleNodeClick,
|
||||
} = useGraphEntitySearch(onNodeClick);
|
||||
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchKeyword(value);
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!selectedTemplateId) return;
|
||||
await deleteDocumentStructureGraph(selectedTemplateId);
|
||||
@@ -71,7 +68,7 @@ function Representation({ onNodeClick }: RepresentationProps) {
|
||||
) : (
|
||||
<ExpandableSearchInput
|
||||
value={searchKeyword}
|
||||
onChange={handleSearchChange}
|
||||
onChange={handleSearchKeywordChange}
|
||||
placeholder={t('chunk.search', 'Search')}
|
||||
/>
|
||||
)}
|
||||
@@ -103,7 +100,6 @@ function Representation({ onNodeClick }: RepresentationProps) {
|
||||
{!(loading && !data) && templates.length > 0 && (
|
||||
<RepresentationRenderer
|
||||
template={selectedTemplate}
|
||||
searchKeyword={searchKeyword}
|
||||
onNodeClick={handleNodeClick}
|
||||
highlightNodeId={highlightNodeId}
|
||||
/>
|
||||
|
||||
@@ -24,10 +24,8 @@ export function buildSkillTreeData(
|
||||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Prunes the skill tree to nodes matching the keyword, keeping the
|
||||
// ancestors of matched nodes so the tree stays navigable.
|
||||
export function filterSkillTreeData(
|
||||
items: TreeDataItem[],
|
||||
keyword: string,
|
||||
|
||||
Reference in New Issue
Block a user