diff --git a/web/package-lock.json b/web/package-lock.json index f9095022f9..8e4891021b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -71,6 +71,7 @@ "classnames": "^2.5.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "d3-force": "^3.0.0", "dayjs": "^1.11.10", "dompurify": "^3.3.2", "embla-carousel-react": "^8.6.0", diff --git a/web/package.json b/web/package.json index 70069c9fd2..08c3ce89ca 100644 --- a/web/package.json +++ b/web/package.json @@ -81,6 +81,7 @@ "classnames": "^2.5.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "d3-force": "^3.0.0", "dayjs": "^1.11.10", "dompurify": "^3.3.2", "embla-carousel-react": "^8.6.0", diff --git a/web/src/components/artifact-force-graph/index.tsx b/web/src/components/artifact-force-graph/index.tsx index bf9eebaf3c..94696b0dbe 100644 --- a/web/src/components/artifact-force-graph/index.tsx +++ b/web/src/components/artifact-force-graph/index.tsx @@ -7,8 +7,13 @@ import { getNodeRadius as defaultGetNodeRadius, MinNodeRadius, } from './node-style'; -import { type ArtifactForceGraphProps, type ArtifactGraphNode } from './types'; +import { + type ArtifactForceGraphProps, + type ArtifactGraphLink, + type ArtifactGraphNode, +} from './types'; import { useArtifactGraphData } from './use-artifact-graph-data'; +import { useCenterGravity } from './use-center-gravity'; import { useContainerDimensions } from './use-container-dimensions'; import { useGraphHighlight } from './use-graph-highlight'; import { defaultMapNodeToValue } from './utils'; @@ -35,6 +40,7 @@ function ArtifactForceGraph({ ); const hasFittedRef = useRef(false); const dimensions = useContainerDimensions(containerRef, show); + const hasDimensions = dimensions.width > 0 && dimensions.height > 0; const graphData = useArtifactGraphData({ data, @@ -43,16 +49,6 @@ function ArtifactForceGraph({ getNodeRadius, }); - const getBaseLinkColor = useCallback(() => { - if (typeof window === 'undefined' || !containerRef.current) { - return '#b2b5b7'; - } - return window - .getComputedStyle(containerRef.current) - .getPropertyValue('--text-disabled') - .trim(); - }, []); - // Resolve the controlled id back to a node object reference (highlighting relies on the node's __neighbors/__links) const pinnedNode = useMemo( () => @@ -69,12 +65,14 @@ function ArtifactForceGraph({ getLinkColor, getLinkWidth, paintNode, - } = useGraphHighlight(getBaseLinkColor, pinnedNode); + } = useGraphHighlight(containerRef, pinnedNode); useEffect(() => { hasFittedRef.current = false; }, [graphData]); + useCenterGravity(fgRef, hasDimensions); + const handleEngineStop = useCallback(() => { if (!hasFittedRef.current && fgRef.current) { fgRef.current.zoomToFit(400); @@ -94,12 +92,24 @@ function ArtifactForceGraph({ [], ); + // Hover tooltip shows the entity description; empty string hides it + const getNodeLabel = useCallback( + (node: ArtifactGraphNode) => node.description ?? '', + [], + ); + + // Empty label hides the tooltip, so relations without a type show nothing + const getLinkLabel = useCallback( + (link: ArtifactGraphLink) => link.type ?? '', + [], + ); + return (
- {dimensions.width > 0 && dimensions.height > 0 && ( + {hasDimensions && ( ({ nodeColor={nodeColor} nodeVal={nodeVal} cooldownTicks={100} - nodeLabel={''} + nodeLabel={getNodeLabel} autoPauseRedraw={false} onEngineStop={handleEngineStop} onNodeClick={handleNodeClick} @@ -118,6 +128,7 @@ function ArtifactForceGraph({ nodeCanvasObjectMode={nodeCanvasObjectMode} linkColor={getLinkColor} linkWidth={getLinkWidth} + linkLabel={getLinkLabel} /> )}
diff --git a/web/src/components/artifact-force-graph/node-label.ts b/web/src/components/artifact-force-graph/node-label.ts index 715a5bd6c3..8f41af3faa 100644 --- a/web/src/components/artifact-force-graph/node-label.ts +++ b/web/src/components/artifact-force-graph/node-label.ts @@ -20,6 +20,6 @@ export const renderNodeLabel: NonNullable< ctx.fillStyle = `rgb(${textSecondary})`; if (typeof graphNode.x === 'number' && typeof graphNode.y === 'number') { - ctx.fillText(label, graphNode.x, graphNode.y + radius - 2); + ctx.fillText(label, graphNode.x, graphNode.y + radius - 9); } }; diff --git a/web/src/components/artifact-force-graph/types.ts b/web/src/components/artifact-force-graph/types.ts index 278cf4a0a7..8d7114227e 100644 --- a/web/src/components/artifact-force-graph/types.ts +++ b/web/src/components/artifact-force-graph/types.ts @@ -14,7 +14,7 @@ export type ArtifactGraphNode = NodeObject & { export type ArtifactGraphLink = LinkObject< ArtifactGraphNode, - { source: string; target: string } + { source: string; target: string; type?: string } >; export interface ArtifactForceGraphProps { diff --git a/web/src/components/artifact-force-graph/use-artifact-graph-data.ts b/web/src/components/artifact-force-graph/use-artifact-graph-data.ts index 1835c955a2..e03e3b01c1 100644 --- a/web/src/components/artifact-force-graph/use-artifact-graph-data.ts +++ b/web/src/components/artifact-force-graph/use-artifact-graph-data.ts @@ -51,6 +51,7 @@ export const useArtifactGraphData = ({ (relation) => ({ source: relation.from, target: relation.to, + type: relation.type, }), ); diff --git a/web/src/components/artifact-force-graph/use-center-gravity.ts b/web/src/components/artifact-force-graph/use-center-gravity.ts new file mode 100644 index 0000000000..2de52d5a4c --- /dev/null +++ b/web/src/components/artifact-force-graph/use-center-gravity.ts @@ -0,0 +1,28 @@ +import { forceX, forceY } from 'd3-force'; +import { useEffect, type RefObject } from 'react'; +import { type ForceGraphMethods } from 'react-force-graph-2d'; +import { type ArtifactGraphNode } from './types'; + +// Weak gravity pulling every node toward the center so disconnected +// components and isolated nodes are not flung far away by charge repulsion. +const CenterGravityStrength = 0.08; + +// Register weak center gravity once the graph is mounted; the forces live +// on the d3 simulation and persist across graphData changes. +export function useCenterGravity( + fgRef: RefObject | undefined>, + hasDimensions: boolean, +) { + useEffect(() => { + const fg = fgRef.current; + if (!hasDimensions || !fg) return; + fg.d3Force( + 'x', + forceX(0).strength(CenterGravityStrength), + ); + fg.d3Force( + 'y', + forceY(0).strength(CenterGravityStrength), + ); + }, [fgRef, hasDimensions]); +} diff --git a/web/src/components/artifact-force-graph/use-graph-highlight.ts b/web/src/components/artifact-force-graph/use-graph-highlight.ts index 6a555c6497..a162b8d471 100644 --- a/web/src/components/artifact-force-graph/use-graph-highlight.ts +++ b/web/src/components/artifact-force-graph/use-graph-highlight.ts @@ -7,14 +7,14 @@ import { HighlightLinkWidth, } from './node-style'; import { type ArtifactGraphLink, type ArtifactGraphNode } from './types'; -import { withAlpha } from './utils'; +import { getBaseLinkColor, withAlpha } from './utils'; type PaintNodeFn = NonNullable< ComponentProps['nodeCanvasObject'] >; export const useGraphHighlight = ( - getBaseLinkColor: () => string, + containerRef: React.RefObject, pinnedNode?: ArtifactGraphNode | null, ) => { const [hoverNode, setHoverNode] = useState(null); @@ -50,12 +50,12 @@ export const useGraphHighlight = ( const getLinkColor = useCallback( (link: ArtifactGraphLink) => { - const baseColor = getBaseLinkColor(); + const baseColor = getBaseLinkColor(containerRef.current); return activeNode && !highlightLinks.has(link) ? withAlpha(baseColor, DimmedAlpha) : baseColor; }, - [getBaseLinkColor, activeNode, highlightLinks], + [containerRef, activeNode, highlightLinks], ); const getLinkWidth = useCallback( diff --git a/web/src/components/artifact-force-graph/utils.ts b/web/src/components/artifact-force-graph/utils.ts index 0dd25a5594..ff782f9781 100644 --- a/web/src/components/artifact-force-graph/utils.ts +++ b/web/src/components/artifact-force-graph/utils.ts @@ -4,6 +4,16 @@ export const defaultMapNodeToValue = ( node: TNode, ): TNode => node; +export const getBaseLinkColor = (element?: HTMLElement | null): string => { + if (typeof window === 'undefined' || !element) { + return '#b2b5b7'; + } + return window + .getComputedStyle(element) + .getPropertyValue('--border-default') + .trim(); +}; + export const withAlpha = (color: string, alpha: number): string => { if (color.length === 7 && color.startsWith('#')) { return ( diff --git a/web/src/components/floating-chat-widget-markdown.tsx b/web/src/components/floating-chat-widget-markdown.tsx index 4bd9e5cbb0..f0ec228129 100644 --- a/web/src/components/floating-chat-widget-markdown.tsx +++ b/web/src/components/floating-chat-widget-markdown.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import DOMPurify from 'dompurify'; import 'katex/dist/katex.min.css'; import { omit } from 'lodash'; -import { pipe } from 'lodash/fp'; +import pipe from 'lodash/fp/pipe'; import { Info } from 'lucide-react'; import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/web/src/components/markdown-content/index.tsx b/web/src/components/markdown-content/index.tsx index f6378bfea0..2f332bc4b5 100644 --- a/web/src/components/markdown-content/index.tsx +++ b/web/src/components/markdown-content/index.tsx @@ -29,7 +29,7 @@ import { } from '@/utils/chat'; import classNames from 'classnames'; import { omit } from 'lodash'; -import { pipe } from 'lodash/fp'; +import pipe from 'lodash/fp/pipe'; import reactStringReplace from 'react-string-replace'; import { LoadingDots } from '../loading-dots'; import { Button } from '../ui/button'; diff --git a/web/src/components/next-markdown-content/index.tsx b/web/src/components/next-markdown-content/index.tsx index b56b584f4b..600c89d6f0 100644 --- a/web/src/components/next-markdown-content/index.tsx +++ b/web/src/components/next-markdown-content/index.tsx @@ -33,7 +33,7 @@ import { useLoadingPause } from '@/hooks/use-loading-pause'; import { cn } from '@/lib/utils'; import classNames from 'classnames'; import { omit } from 'lodash'; -import { pipe } from 'lodash/fp'; +import pipe from 'lodash/fp/pipe'; import reactStringReplace from 'react-string-replace'; import { LoadingDots } from '../loading-dots'; import { Button } from '../ui/button'; diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx index 38fa6ea5c3..dc71deeae1 100644 --- a/web/src/components/originui/select-with-search.tsx +++ b/web/src/components/originui/select-with-search.tsx @@ -2,6 +2,7 @@ import { CheckIcon, ChevronDownIcon, XIcon } from 'lucide-react'; import { + KeyboardEvent, MouseEventHandler, ReactNode, forwardRef, @@ -54,6 +55,8 @@ export type SelectWithSearchFlagProps = { placeholder?: string; emptyData?: string; allowCustomValue?: boolean; + onNoMatchEnter?(searchValue: string): void; + disableAutoSelectOnEnter?: boolean; testId?: string; optionTestIdPrefix?: string; }; @@ -81,6 +84,36 @@ function findLabelWithOptions( .filter(Boolean)[0]?.label; } +function hasMatchingOptions( + options: SelectWithSearchFlagOptionType[], + searchValue: string, +) { + const search = searchValue.trim(); + if (!search) { + return true; + } + return options.some((group) => { + if (group.options) { + return group.options.some( + (option) => + filterFn( + option.value ?? '', + search, + typeof option.label === 'string' ? [option.label] : [], + ) === 1, + ); + } + return ( + filterFn( + group.value ?? '', + search, + group.keywords ?? + (typeof group.label === 'string' ? [group.label] : []), + ) === 1 + ); + }); +} + export const SelectWithSearch = forwardRef< React.ElementRef, SelectWithSearchFlagProps @@ -96,6 +129,8 @@ export const SelectWithSearch = forwardRef< placeholder = t('common.selectPlaceholder'), emptyData = t('common.noDataFound'), allowCustomValue = false, + onNoMatchEnter, + disableAutoSelectOnEnter = false, testId, optionTestIdPrefix, }, @@ -176,6 +211,23 @@ export const SelectWithSearch = forwardRef< [onChange], ); + const handleInputKeyDown = useCallback( + (e: KeyboardEvent) => { + const keywords = searchValue.trim(); + if (e.key === 'Enter' && keywords) { + if (disableAutoSelectOnEnter) { + e.preventDefault(); + onNoMatchEnter?.(keywords); + setSearchValue(''); + setOpen(false); + } else if (!hasMatchingOptions(options, keywords)) { + onNoMatchEnter?.(keywords); + } + } + }, + [searchValue, options, onNoMatchEnter, disableAutoSelectOnEnter], + ); + useEffect(() => { setValue(val); }, [val]); @@ -235,6 +287,7 @@ export const SelectWithSearch = forwardRef< className=" placeholder:text-text-disabled" value={searchValue} onValueChange={setSearchValue} + onKeyDown={handleInputKeyDown} /> )} diff --git a/web/src/pages/chunk/representation/utils/adapters.ts b/web/src/components/structure-graph/adapters.ts similarity index 97% rename from web/src/pages/chunk/representation/utils/adapters.ts rename to web/src/components/structure-graph/adapters.ts index a566030f9c..9de4848f6b 100644 --- a/web/src/pages/chunk/representation/utils/adapters.ts +++ b/web/src/components/structure-graph/adapters.ts @@ -16,9 +16,13 @@ declare module '@/components/ui/tree-view' { } } +export function getEntityDisplayName(entity: IStructureGraphEntity) { + return entity.name ?? entity.id ?? ''; +} + function normalizeEntity(entity: IStructureGraphEntity) { const id = entity.id ?? entity.name ?? ''; - const name = entity.name ?? entity.id ?? ''; + const name = getEntityDisplayName(entity); return { ...entity, id, name }; } @@ -208,6 +212,7 @@ export function adaptKnowledgeGraphToForceGraph( .map((relation) => ({ from: relation.from, to: relation.to, + type: relation.type ?? '', })), }; } diff --git a/web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx b/web/src/components/structure-graph/mindmap-g6-graph/index.tsx similarity index 98% rename from web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx rename to web/src/components/structure-graph/mindmap-g6-graph/index.tsx index c2ea3a4bc8..5bd83e2f56 100644 --- a/web/src/pages/chunk/representation/components/mindmap-g6-graph/index.tsx +++ b/web/src/components/structure-graph/mindmap-g6-graph/index.tsx @@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'; import { Graph, IElementEvent, NodeEvent, treeToGraphData } from '@antv/g6'; import { memo, useEffect, useRef, useState } from 'react'; -import { adaptMindMapToIndentedTree } from '../../utils/adapters'; +import { adaptMindMapToIndentedTree } from '../adapters'; import { type MindMapG6GraphProps, type MindMapNodeValue } from './types'; interface MindMapNodeData { diff --git a/web/src/pages/chunk/representation/components/mindmap-g6-graph/types.ts b/web/src/components/structure-graph/mindmap-g6-graph/types.ts similarity index 100% rename from web/src/pages/chunk/representation/components/mindmap-g6-graph/types.ts rename to web/src/components/structure-graph/mindmap-g6-graph/types.ts diff --git a/web/src/pages/chunk/representation/components/representation-renderer.tsx b/web/src/components/structure-graph/representation-renderer.tsx similarity index 88% rename from web/src/pages/chunk/representation/components/representation-renderer.tsx rename to web/src/components/structure-graph/representation-renderer.tsx index 085831257b..613ef7c822 100644 --- a/web/src/pages/chunk/representation/components/representation-renderer.tsx +++ b/web/src/components/structure-graph/representation-renderer.tsx @@ -1,7 +1,10 @@ import ArtifactForceGraph from '@/components/artifact-force-graph'; import { TreeView, type TreeDataItem } from '@/components/ui/tree-view'; import { CompilationTemplateKind } from '@/constants/compilation'; -import { type IArtifactGraphEntity } from '@/interfaces/database/dataset'; +import { + type IArtifactGraph, + type IArtifactGraphEntity, +} from '@/interfaces/database/dataset'; import { type IStructureGraphTemplate, type StructureTemplateKind, @@ -14,7 +17,7 @@ import { adaptTimelineToX6Data, adaptTreeToTreeData, filterTreeDataByKeyword, -} from '../utils/adapters'; +} from './adapters'; import MindMapG6Graph from './mindmap-g6-graph'; import TimelineX6Graph from './timeline-x6-graph'; @@ -24,10 +27,13 @@ export interface ClickableNode { source_chunk_ids?: string[]; } +const EmptyForceGraphData: IArtifactGraph = { entities: [], relations: [] }; + interface RepresentationRendererProps { template?: IStructureGraphTemplate; searchKeyword?: string; onNodeClick?: (node: ClickableNode) => void; + highlightNodeId?: string | null; } function UnsupportedPlaceholder({ kind }: { kind: StructureTemplateKind }) { @@ -47,6 +53,7 @@ export function RepresentationRenderer({ template, searchKeyword = '', onNodeClick, + highlightNodeId, }: RepresentationRendererProps) { const handleTreeItemClick = useCallback( (item: TreeDataItem | undefined) => { @@ -114,6 +121,16 @@ export function RepresentationRenderer({ return []; }, [template, searchKeyword]); + // Keep a stable reference across re-renders so the memoized ArtifactForceGraph + // does not restart its force simulation when only highlightNodeId changes + const forceGraphData = useMemo( + () => + template + ? adaptKnowledgeGraphToForceGraph(template) + : EmptyForceGraphData, + [template], + ); + if (!template) { return null; } @@ -143,10 +160,11 @@ export function RepresentationRenderer({ return (
); @@ -174,7 +192,7 @@ export function RepresentationRenderer({ return (
{ [navigate], ); + const navigateToCompilationTemplateEditNext = useCallback( + (id?: string) => () => { + if (id && id !== 'create') { + navigate(`${Routes.CompilationTemplatesEditNext}/${id}`); + } else { + navigate(Routes.CompilationTemplatesEditNext); + } + }, + [navigate], + ); + return { navigateToDatasetList, navigateToDataset, @@ -253,5 +264,6 @@ export const useNavigatePage = () => { navigateToModelSetting, navigateToCompilationTemplates, navigateToCompilationTemplate, + navigateToCompilationTemplateEditNext, }; }; diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index b9ede5ec69..7c40c4274e 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -139,8 +139,8 @@ export const useFetchAgentListByPage = () => { const debouncedSearchString = useDebounce(searchString, { wait: 500 }); const { filterValue, handleFilterSubmit } = useHandleFilterSubmit(); const canvasCategory = Array.isArray(filterValue.canvasCategory) - ? filterValue.canvasCategory - : []; + ? (filterValue.canvasCategory[0] as string | undefined) + : undefined; const owner = filterValue.owner; const tags = Array.isArray(filterValue.tags) ? filterValue.tags : undefined; @@ -148,7 +148,7 @@ export const useFetchAgentListByPage = () => { page: pagination.current, pageSize: pagination.pageSize, keywords: debouncedSearchString, - canvasCategory: canvasCategory.length === 1 ? canvasCategory[0] : undefined, + canvasCategory, ownerIds: Array.isArray(owner) ? owner : undefined, tags, }); @@ -197,7 +197,7 @@ export const useFetchAgentListByPage = () => { loading, searchString, handleInputChange: onInputChange, - pagination: { ...pagination, total: data?.total }, + pagination: { ...pagination, total: data?.total ?? 0 }, setPagination, filterValue, handleFilterSubmit, diff --git a/web/src/hooks/use-compilation-template-group-request.ts b/web/src/hooks/use-compilation-template-group-request.ts index a00637c90b..6f28841e2d 100644 --- a/web/src/hooks/use-compilation-template-group-request.ts +++ b/web/src/hooks/use-compilation-template-group-request.ts @@ -22,6 +22,7 @@ import { useGetPaginationWithRouter, useHandleSearchChange, } from './logic-hooks'; +import { AgentApiAction } from './use-agent-request'; export const enum CompilationTemplateGroupApiAction { FetchCompilationTemplateGroups = 'fetchCompilationTemplateGroups', @@ -225,6 +226,10 @@ export const useDeleteCompilationTemplateGroup = () => { CompilationTemplateGroupApiAction.FetchCompilationTemplateGroups, ], }); + // The agents page lists groups merged into /agents results. + queryClient.invalidateQueries({ + queryKey: [AgentApiAction.FetchAgentListByPage], + }); } return data?.data ?? true; }, diff --git a/web/src/hooks/use-compilation-template-request.ts b/web/src/hooks/use-compilation-template-request.ts index 438e5c5bc9..017150901e 100644 --- a/web/src/hooks/use-compilation-template-request.ts +++ b/web/src/hooks/use-compilation-template-request.ts @@ -1,4 +1,5 @@ import message from '@/components/ui/message'; +import { CompilationTemplateKind } from '@/constants/compilation'; import { ICompilationTemplate, ICompilationTemplateBuiltin, @@ -52,6 +53,11 @@ export const CompilationTemplateKeys = { wikiPresets: () => [CompilationTemplateApiAction.FetchWikiPresets] as const, }; +const ExcludedBuiltinKinds: string[] = [ + CompilationTemplateKind.SessionEssence, + CompilationTemplateKind.SessionGraph, +]; + export const useFetchCompilationTemplatesByPage = () => { const { searchString, handleInputChange } = useHandleSearchChange(); const { pagination, setPagination } = useGetPaginationWithRouter(); @@ -129,7 +135,9 @@ export const useFetchBuiltinCompilationTemplates = () => { gcTime: 0, queryFn: async () => { const { data } = await listBuiltinCompilationTemplates(); - return (data?.data ?? []) as ICompilationTemplateBuiltin[]; + return ((data?.data ?? []) as ICompilationTemplateBuiltin[]).filter( + (template) => !ExcludedBuiltinKinds.includes(template.kind), + ); }, }, ); diff --git a/web/src/hooks/use-document-request.ts b/web/src/hooks/use-document-request.ts index 316975585a..af37f2e51a 100644 --- a/web/src/hooks/use-document-request.ts +++ b/web/src/hooks/use-document-request.ts @@ -29,7 +29,12 @@ import kbService, { } from '@/services/knowledge-service'; import { restAPIv1 } from '@/utils/api'; import { buildChunkHighlights } from '@/utils/document-util'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import { useDebounce } from 'ahooks'; import { get } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -83,6 +88,17 @@ export const DocumentStructureKeys = { datasetId, documentId, ] as const, + graphWithKeywords: ( + datasetId: string, + documentId: string, + keywords: string, + ) => + [ + DocumentStructureApiAction.FetchDocumentStructureGraph, + datasetId, + documentId, + keywords, + ] as const, }; export const useUploadDocument = () => { @@ -718,21 +734,30 @@ export const useFetchDocumentThumbnailsByIds = () => { return { data, setDocumentIds }; }; -export function useFetchDocumentStructureGraph() { +export function useFetchDocumentStructureGraph(keywords?: string) { const { knowledgeId: datasetId, documentId } = useGetKnowledgeSearchParams(); const enabled = !!datasetId && !!documentId; + const trimmedKeywords = keywords?.trim(); const { data, isFetching: loading } = useQuery({ - queryKey: DocumentStructureKeys.graph(datasetId, documentId), + queryKey: trimmedKeywords + ? DocumentStructureKeys.graphWithKeywords( + datasetId, + documentId, + trimmedKeywords, + ) + : DocumentStructureKeys.graph(datasetId, documentId), enabled, initialData: null, gcTime: 0, + placeholderData: keepPreviousData, queryFn: async () => { const { data } = await documentStructureService.getDocumentStructureGraph( datasetId, documentId, + trimmedKeywords, ); return data?.data ?? null; }, diff --git a/web/src/hooks/use-knowledge-request.ts b/web/src/hooks/use-knowledge-request.ts index 6d2009c635..8ecba3f6a0 100644 --- a/web/src/hooks/use-knowledge-request.ts +++ b/web/src/hooks/use-knowledge-request.ts @@ -2,8 +2,11 @@ import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-f import message from '@/components/ui/message'; import { ParseType } from '@/constants/knowledge'; import { ResponsePostType, ResponseType } from '@/interfaces/database/base'; +import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants'; +import { DatasetGenerateKeys } from '@/pages/dataset/dataset/generate-button/hook'; import { IArtifact, + IArtifactAlteration, IArtifactGraph, IArtifactPage, IArtifactTopic, @@ -18,6 +21,7 @@ import { IWikiCommitDetail, IWikiCommitListResponse, } from '@/interfaces/database/dataset'; +import { type IStructureGraphResponse } from '@/interfaces/database/document-structure'; import { IFetchArtifactGraphRequestParams, ITestRetrievalRequestBody, @@ -27,8 +31,10 @@ import i18n from '@/locales/config'; import kbService, { clearWiki, deleteKnowledgeGraph, + getArtifactsAlteration, getArtifactGraph, getArtifactPage, + getArtifactsStructure, getKbDetail, getKnowledgeGraph, getWikiCommit, @@ -40,10 +46,12 @@ import kbService, { listWikiCommits, removeTag, renameTag, + runIndex, updateArtifactPage, updateKb, } from '@/services/knowledge-service'; import { + keepPreviousData, useInfiniteQuery, useIsMutating, useMutation, @@ -86,6 +94,9 @@ export const enum KnowledgeApiAction { FetchKnowledgeList = 'fetchKnowledgeList', RemoveKnowledgeGraph = 'removeKnowledgeGraph', ClearWiki = 'clearWiki', + FetchDatasetStructure = 'fetchDatasetStructure', + FetchArtifactAlteration = 'fetchArtifactAlteration', + RunArtifactIndex = 'runArtifactIndex', } export const useKnowledgeBaseId = (): string => { @@ -436,6 +447,28 @@ export const ArtifactTopicKeys = { [KnowledgeApiAction.FetchArtifactTopicList, datasetId] as const, }; +export const ArtifactAlterationKeys = { + detail: (datasetId: string) => + [KnowledgeApiAction.FetchArtifactAlteration, datasetId] as const, +}; + +export function useFetchArtifactAlteration() { + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, isFetching: loading } = useQuery({ + queryKey: ArtifactAlterationKeys.detail(knowledgeBaseId), + initialData: null, + enabled: !!knowledgeBaseId, + gcTime: 0, + queryFn: async () => { + const { data } = await getArtifactsAlteration(knowledgeBaseId); + return data?.data ?? null; + }, + }); + + return { data, loading }; +} + const wikiCommitKeys = { list: (datasetId: string, pageType: string, slug: string) => [KnowledgeApiAction.FetchWikiCommits, datasetId, pageType, slug] as const, @@ -725,7 +758,13 @@ export function useFetchKnowledgeGraph() { export const artifactGraphKeys = { graph: (datasetId: string, params?: IFetchArtifactGraphRequestParams) => - [KnowledgeApiAction.FetchArtifactGraph, datasetId, params?.node] as const, + [ + KnowledgeApiAction.FetchArtifactGraph, + datasetId, + params?.node, + params?.keywords, + params?.top_n, + ] as const, }; export function useFetchArtifactGraph( @@ -737,6 +776,7 @@ export function useFetchArtifactGraph( const { data, isFetching: loading } = useQuery({ queryKey: artifactGraphKeys.graph(knowledgeBaseId, params), initialData: { entities: [], relations: [] } as IArtifactGraph, + placeholderData: keepPreviousData, enabled: !!knowledgeBaseId && (options?.enabled ?? true), gcTime: 0, queryFn: async () => { @@ -748,6 +788,51 @@ export function useFetchArtifactGraph( return { data, loading }; } +export const DatasetStructureKeys = { + all: (datasetId: string) => + [KnowledgeApiAction.FetchDatasetStructure, datasetId] as const, + kind: (datasetId: string, kind: string) => + [KnowledgeApiAction.FetchDatasetStructure, datasetId, kind] as const, + kindWithKeywords: (datasetId: string, kind: string, keywords: string) => + [ + KnowledgeApiAction.FetchDatasetStructure, + datasetId, + kind, + keywords, + ] as const, +}; + +export function useFetchDatasetStructureGraph(kind: string, keywords?: string) { + const knowledgeBaseId = useKnowledgeBaseId(); + const enabled = !!knowledgeBaseId && !!kind; + const trimmedKeywords = keywords?.trim(); + + const { data, isFetching: loading } = + useQuery({ + queryKey: trimmedKeywords + ? DatasetStructureKeys.kindWithKeywords( + knowledgeBaseId, + kind, + trimmedKeywords, + ) + : DatasetStructureKeys.kind(knowledgeBaseId, kind), + initialData: null, + enabled, + gcTime: 0, + placeholderData: keepPreviousData, + queryFn: async () => { + const { data } = await getArtifactsStructure( + knowledgeBaseId, + kind, + trimmedKeywords, + ); + return (data?.data as IStructureGraphResponse | null) ?? null; + }, + }); + + return { data, loading }; +} + export function useFetchKnowledgeMetadata(kbIds: string[] = []) { const { data, isFetching: loading } = useQuery< Record> @@ -841,6 +926,43 @@ export const useClearWiki = () => { return { data, loading, clearWiki: mutateAsync }; }; +export const useRunArtifactIndex = () => { + const knowledgeBaseId = useKnowledgeBaseId(); + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [KnowledgeApiAction.RunArtifactIndex], + mutationFn: async () => { + const { data } = await runIndex(knowledgeBaseId, 'artifact'); + if (data?.code === 0) { + message.success(i18n.t('message.operated')); + queryClient.invalidateQueries({ + queryKey: ArtifactAlterationKeys.detail(knowledgeBaseId), + }); + queryClient.invalidateQueries({ + queryKey: ArtifactKeys.listByDataset(knowledgeBaseId), + }); + queryClient.invalidateQueries({ + queryKey: ArtifactTopicKeys.listByDataset(knowledgeBaseId), + }); + queryClient.invalidateQueries({ + queryKey: DatasetGenerateKeys.traceById( + GenerateType.Artifact, + knowledgeBaseId, + ), + }); + } + return data; + }, + }); + + return { data, loading, runArtifactIndex: mutateAsync }; +}; + const KNOWLEDGE_LIST_PAGE_SIZE = 10; export const KnowledgeListKeys = { diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index abf84eebd5..21011904fb 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -33,6 +33,7 @@ export interface ISwitchForm { import { AgentCategory } from '@/constants/agent'; import { Edge, Node } from '@xyflow/react'; import { IReference, Message } from './chat'; +import { ICompilationTemplateGroup } from './compilation-template'; import { IDataset } from './dataset'; export type DSLComponents = Record; @@ -85,6 +86,19 @@ export declare interface IFlow { tags?: string; } +// GET /agents merges compilation template groups into the agent list when no +// canvas_category is requested; every item carries this discriminator. +export enum AgentListItemType { + Agent = 'agent', + CompilationTemplateGroup = 'compilation_template_group', +} + +export type AgentListItem = + | (IFlow & { type?: AgentListItemType.Agent }) + | (ICompilationTemplateGroup & { + type: AgentListItemType.CompilationTemplateGroup; + }); + export interface IFlowTemplate { avatar: string; canvas_type: string; diff --git a/web/src/interfaces/database/dataset.ts b/web/src/interfaces/database/dataset.ts index 7a729c5eab..9807d2a55a 100644 --- a/web/src/interfaces/database/dataset.ts +++ b/web/src/interfaces/database/dataset.ts @@ -289,9 +289,19 @@ export interface IArtifactGraphEntity { source_chunk_ids?: string[]; } +export interface IArtifactAlteration { + removed: number; + newly_uploaded: number; + removed_doc_ids: string[]; + newly_uploaded_doc_ids: string[]; + involved_doc_ids: string[]; + eligible_doc_ids: string[]; +} + export interface IArtifactGraphRelation { from: string; to: string; + type?: string; } export interface IArtifactGraph { diff --git a/web/src/interfaces/request/knowledge.ts b/web/src/interfaces/request/knowledge.ts index 92edc54c07..1e9c71fb4a 100644 --- a/web/src/interfaces/request/knowledge.ts +++ b/web/src/interfaces/request/knowledge.ts @@ -58,6 +58,8 @@ export interface IFetchArtifactTopicListRequestParams { export interface IFetchArtifactGraphRequestParams { node?: string; + keywords?: string; + top_n?: number; } export interface IUpdateArtifactPageRequestBody { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 7d83c155a1..1a9a40a054 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -465,6 +465,10 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim clearWikiTitle: 'Clear wiki', clearWikiDescription: 'Are you sure you want to clear all wiki pages in this dataset? This action cannot be undone.', + update: 'Update', + updateTooltip: + '{{newlyUploaded}} new, {{removed}} removed documents found. Click to compile and merge into current Wiki.', + updateSheetTitle: 'Update Wiki', noSkills: 'No skills yet', generate: 'Generate', raptor: 'RAPTOR', @@ -520,8 +524,19 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim versionContentRequired: 'Please input version content', graph: 'Graph', graphPlaceholder: 'Graph view placeholder', - llmWiki: 'LLM Wiki', + skills: 'To Skills', + llmWiki: 'Wiki', navTree: 'Tree', + structureGraph: 'Graph', + structureMindmap: 'Mind map', + structureTimeline: 'Timeline', + structureSessionEssence: 'Session essence', + structureSessionGraph: 'Session graph', + noStructureGraph: 'No graph yet', + noStructureMindmap: 'No mind map yet', + noStructureTimeline: 'No timeline yet', + noStructureSessionEssence: 'No session essence yet', + noStructureSessionGraph: 'No session graph yet', contents: 'Navigation', topics: 'Topics', concept: 'Concept', @@ -932,7 +947,7 @@ The above is the content you need to summarize.`, randomSeed: 'Random seed', randomSeedMessage: 'Random seed is required', entityTypes: 'Entity types', - compilationTemplate: 'Compilation template', + compilationTemplate: 'Operator', scopeFile: 'File', vietnamese: 'Vietnamese', pageRank: 'Page rank', @@ -1828,8 +1843,8 @@ Example: Virtual Hosted Style`, chatChannelsDescription: 'Manage your chat channel bots and credentials', compilationTemplates: 'Compilation templates', compilationTemplatesDescription: 'Manage your compilation templates', - addTemplateGroup: 'Add template group', - editTemplateGroup: 'Edit template group', + addTemplateGroup: 'Add template', + editTemplateGroup: 'Edit template', groupName: 'Group name', groupNameRequired: 'Please input group name', groupDescription: 'Group description', @@ -1848,7 +1863,7 @@ Example: Virtual Hosted Style`, templateName: 'Name', templateNameRequired: 'Please input template name', templateDescription: 'Description', - llmForExtraction: 'LLM for extraction', + llmForExtraction: 'Default Model for extraction', llmForExtractionRequired: 'Please select an LLM model', templateKind: 'Kind', templateKindRequired: 'Please select a kind', @@ -1888,6 +1903,7 @@ Example: Virtual Hosted Style`, templateWizardConfigurationDescription: 'Configure templates', blueprints: 'Blueprints', blueprintsDescription: 'Select required blueprints', + custom: 'Custom', templates: 'Templates', addFieldModalTitle: 'Add field', editFieldModalTitle: 'Edit field', @@ -3193,6 +3209,13 @@ This process aggregates variables from multiple branches into a single variable copyOfAgentName: '{{name}} (copy)', ceateAgent: 'Workflow', createPipeline: 'Ingestion pipeline', + createIngestionPipeline: 'Create ingestion pipeline', + createWorkflow: 'Create workflow', + tabList: { + ingestionPipeline: 'Ingestion pipeline', + compilationOperator: 'Compilation operator', + workflow: 'Workflow', + }, chooseAgentType: 'Choose agent type', parser: 'Parser', parserDescription: @@ -3210,9 +3233,9 @@ This process aggregates variables from multiple branches into a single variable extractor: 'Transformer', extractorDescription: 'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.', - compiler: 'Operator', + compiler: 'Compiler', compilerDescription: - 'Processes document chunks using operator templates into structured artifacts.', + 'Compiles document chunks using knowledge compilation templates into structured artifacts.', outputFormat: 'Output format', fileFormats: 'File type', fileFormatOptions: { diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index e636fcec9b..4cbf8c715f 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1659,6 +1659,7 @@ export default { templateWizardConfigurationDescription: 'テンプレートを設定します', blueprints: 'ブループリント', blueprintsDescription: '必要なブループリントを選択してください', + custom: 'カスタム', templates: 'テンプレート', addFieldModalTitle: 'フィールドを追加', editFieldModalTitle: 'フィールドを編集', @@ -2863,6 +2864,11 @@ export default { copyOfAgentName: '{{name}} (コピー)', ceateAgent: 'ワークフロー', createPipeline: '取り込みパイプライン', + tabList: { + ingestionPipeline: '取り込みパイプライン', + compilationOperator: 'コンパイルオペレーター', + workflow: 'ワークフロー', + }, chooseAgentType: 'エージェントタイプを選択', parser: 'パーサー', parserDescription: diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 736b637a2a..893ae64187 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -415,6 +415,10 @@ export default { clearWikiTitle: '清空 Wiki', clearWikiDescription: '确定要清空该数据集下的所有 Wiki 页面吗?此操作无法撤销。', + update: '更新', + updateTooltip: + '发现 {{newlyUploaded}} 个新文档,{{removed}} 个已移除文档。点击编译并合并到当前 Wiki。', + updateSheetTitle: '更新 Wiki', noSkills: '暂无技能', processingType: '处理类型', dataPipeline: '切换或配置 ingestion pipeline。', @@ -465,8 +469,19 @@ export default { versionContentRequired: '请输入版本内容', graph: '图谱', graphPlaceholder: '图谱视图占位', - llmWiki: 'LLM Wiki', + skills: '技能', + llmWiki: 'Wiki', navTree: '目录树', + structureGraph: '图谱', + structureMindmap: '思维导图', + structureTimeline: '时间线', + structureSessionEssence: '会话摘要', + structureSessionGraph: '会话图谱', + noStructureGraph: '暂无图谱', + noStructureMindmap: '暂无思维导图', + noStructureTimeline: '暂无时间线', + noStructureSessionEssence: '暂无会话摘要', + noStructureSessionGraph: '暂无会话图谱', contents: '导航', topics: '主题', concept: '概念', @@ -843,7 +858,7 @@ export default { '在 RAPTOR 中,数据块会根据它们的语义相似性进行聚类。阈值设定了数据块被分到同一组所需的最小相似度。阈值越高,每个聚类中的数据块越少;阈值越低,则每个聚类中的数据块越多。', maxClusterTip: '最多可创建的聚类数。', entityTypes: '实体类型', - compilationTemplate: '编译模板', + compilationTemplate: '算子', scopeFile: '文件', pageRank: '页面排名', pageRankTip: `知识库检索时,你可以为特定知识库设置较高的 PageRank 分数,该知识库中匹配文本块的混合相似度得分会自动叠加 PageRank 分数,从而提升排序权重。详见 https://ragflow.io/docs/dev/set_page_rank。`, @@ -1518,8 +1533,8 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 chatChannelsDescription: '管理您的聊天渠道机器人及凭证', compilationTemplates: '知识编译模板', compilationTemplatesDescription: '管理您的知识编译模板', - addTemplateGroup: '添加模板分组', - editTemplateGroup: '编辑模板分组', + addTemplateGroup: '添加模板', + editTemplateGroup: '编辑模板', groupName: '分组名称', groupNameRequired: '请输入分组名称', groupDescription: '分组描述', @@ -1537,7 +1552,7 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 templateName: '名称', templateNameRequired: '请输入模板名称', templateDescription: '描述', - llmForExtraction: '用于提取的 LLM', + llmForExtraction: '默认提取模型', llmForExtractionRequired: '请选择 LLM 模型', templateKind: '类型', templateKindRequired: '请选择类型', @@ -1577,6 +1592,7 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 templateWizardConfigurationDescription: '管理模板的配置', blueprints: '蓝图', blueprintsDescription: '选择所需要的 blueprints', + custom: '自定义', templates: '模板', addFieldModalTitle: '添加字段', editFieldModalTitle: '编辑字段', @@ -2787,6 +2803,14 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 duplicate: '复制', copyOfAgentName: '{{name}} (副本)', chooseAgentType: '选择智能体类型', + createPipeline: '数据管道', + createIngestionPipeline: '创建数据管道', + createWorkflow: '创建工作流', + tabList: { + ingestionPipeline: '数据管道', + compilationOperator: '编译算子', + workflow: '工作流', + }, parser: '解析器', parserDescription: '从文件中提取原始文本和结构以供下游处理。', tokenizer: '分词器', @@ -2802,8 +2826,8 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 extractor: '提取器', extractorDescription: '使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。', - compiler: '算子', - compilerDescription: '使用算子模板将文档块处理为结构化工件。', + compiler: '编译器', + compilerDescription: '使用知识编译模板将文档块编译为知识工件。', outputFormat: '输出格式', fileFormats: '文件类型', fileFormatOptions: { diff --git a/web/src/pages/agent/canvas/node/compilation-node.tsx b/web/src/pages/agent/canvas/node/compilation-node.tsx index 6f5009f9a1..409926d0ef 100644 --- a/web/src/pages/agent/canvas/node/compilation-node.tsx +++ b/web/src/pages/agent/canvas/node/compilation-node.tsx @@ -2,7 +2,7 @@ import { useCompilationTemplateGroupOptions } from '@/hooks/use-compilation-temp import { IRagNode } from '@/interfaces/database/agent'; import { NodeProps } from '@xyflow/react'; import { get } from 'lodash'; -import { LabelCard, LLMLabelCard } from './card'; +import { LabelCard } from './card'; import { RagNode } from './index'; import { useTranslation } from 'react-i18next'; @@ -17,7 +17,6 @@ export function CompilationNode({ ...props }: NodeProps) { return (
- {t('knowledgeConfiguration.compilationTemplate')} diff --git a/web/src/pages/agent/constant/pipeline.tsx b/web/src/pages/agent/constant/pipeline.tsx index 50be0ed07b..90348f3cfc 100644 --- a/web/src/pages/agent/constant/pipeline.tsx +++ b/web/src/pages/agent/constant/pipeline.tsx @@ -362,7 +362,6 @@ export const initialExtractorValues = { }; export const initialCompilationValues = { - ...initialLlmBaseValues, compilation_template_group_ids: [], outputs: { chunks: { type: 'Array', value: [] }, diff --git a/web/src/pages/agent/form/compilation-form/index.tsx b/web/src/pages/agent/form/compilation-form/index.tsx index 49caf0da53..c3cf9cccf7 100644 --- a/web/src/pages/agent/form/compilation-form/index.tsx +++ b/web/src/pages/agent/form/compilation-form/index.tsx @@ -1,13 +1,10 @@ import { CompilationTemplateFormField } from '@/components/compilation-template-form-field'; -import { LargeModelFormField } from '@/components/large-model-form-field'; -import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { Form } from '@/components/ui/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { memo } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { initialCompilationValues } from '../../constant/pipeline'; -import { useOwnerTenantId } from '../../context'; import { useFormValues } from '../../hooks/use-form-values'; import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { INextOperatorForm } from '../../interface'; @@ -17,7 +14,6 @@ import { Output } from '../components/output'; export const FormSchema = z.object({ compilation_template_group_ids: z.string().optional(), - ...LlmSettingSchema, }); export type CompilationFormSchemaType = z.infer; @@ -26,7 +22,6 @@ const outputList = buildOutputList(initialCompilationValues.outputs); const CompilationForm = ({ node }: INextOperatorForm) => { const defaultValues = useFormValues(initialCompilationValues, node); - const ownerTenantId = useOwnerTenantId(); const form = useForm({ defaultValues, @@ -38,9 +33,6 @@ const CompilationForm = ({ node }: INextOperatorForm) => { return (
- diff --git a/web/src/pages/agent/hooks/use-add-node.ts b/web/src/pages/agent/hooks/use-add-node.ts index 2c86bfd711..4d7e6fe9a9 100644 --- a/web/src/pages/agent/hooks/use-add-node.ts +++ b/web/src/pages/agent/hooks/use-add-node.ts @@ -183,10 +183,7 @@ export const useInitializeOperatorParams = () => { sys_prompt: t('flow.prompts.system.summary'), prompts: t('flow.prompts.user.summary'), }, - [Operator.Compilation]: { - ...initialCompilationValues, - llm_id: llmId, - }, + [Operator.Compilation]: initialCompilationValues, [Operator.DataOperations]: initialDataOperationsValues, [Operator.ListOperations]: initialListOperationsValues, [Operator.VariableAssigner]: initialVariableAssignerValues, diff --git a/web/src/pages/agents/agent-card.tsx b/web/src/pages/agents/agent-card.tsx index 67304c8550..fc27b79bba 100644 --- a/web/src/pages/agents/agent-card.tsx +++ b/web/src/pages/agents/agent-card.tsx @@ -5,15 +5,41 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { AgentCategory } from '@/constants/agent'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; -import { IFlow } from '@/interfaces/database/agent'; -import { Route } from 'lucide-react'; +import { AgentListItemType, IFlow } from '@/interfaces/database/agent'; +import { LucideIcon, Network, Route, Shapes } from 'lucide-react'; import { AgentDropdown } from './agent-dropdown'; import { useRenameAgent } from './use-rename-agent'; export type DatasetCardProps = { - data: IFlow; + data: IFlow & { type?: AgentListItemType }; } & Pick, 'showAgentRenameModal'>; +const CanvasCategoryIconMap: Record = { + [AgentCategory.AgentCanvas]: Network, + [AgentCategory.DataflowCanvas]: Route, +}; + +function AgentTypeIcon({ + data, +}: { + data: IFlow & { type?: AgentListItemType }; +}) { + const Icon = + data.type === AgentListItemType.CompilationTemplateGroup + ? Shapes + : CanvasCategoryIconMap[data.canvas_category]; + + if (!Icon) { + return null; + } + + return ( + + ); +} + function AgentTags({ tags }: { tags?: string }) { const list = (tags || '') .split(',') @@ -55,13 +81,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { // : navigateToAgent(data?.id, data.canvas_category as AgentCategory) } - icon={ - data.canvas_category === AgentCategory.DataflowCanvas && ( - - ) - } + icon={} extra={} showReleaseTime /> diff --git a/web/src/pages/agents/agent-templates.tsx b/web/src/pages/agents/agent-templates.tsx index 493a724052..cdeb951ae2 100644 --- a/web/src/pages/agents/agent-templates.tsx +++ b/web/src/pages/agents/agent-templates.tsx @@ -116,6 +116,7 @@ export default function AgentTemplates() { loading={loading} visible={creatingVisible} hideModal={hideCreatingModal} + canvasCategory={template?.canvas_category as AgentCategory} onOk={handleOk} > )} diff --git a/web/src/pages/agents/compilation-template-card.tsx b/web/src/pages/agents/compilation-template-card.tsx new file mode 100644 index 0000000000..63a01eaea8 --- /dev/null +++ b/web/src/pages/agents/compilation-template-card.tsx @@ -0,0 +1,80 @@ +import { MoreButton } from '@/components/more-button'; +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent } from '@/components/ui/card'; +import { CompilationTemplateScope } from '@/constants/compilation'; +import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template'; +import { Database, FileText, LucideIcon } from 'lucide-react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { formatKindLabel } from '@/utils/compilation-template-util'; +import { CompilationTemplateDropdown } from './compilation-template-dropdown'; + +type CompilationTemplateCardProps = { + data: ICompilationTemplateGroup; + onClick?: () => void; + onDelete: (id: string) => void; +}; + +const ScopeIconMap: Record = { + [CompilationTemplateScope.File]: FileText, + [CompilationTemplateScope.Dataset]: Database, +}; + +function ScopeIcon({ scope }: { scope?: string }) { + const Icon = scope ? ScopeIconMap[scope] : null; + + return Icon ? : null; +} + +export function CompilationTemplateCard({ + data, + onClick, + onDelete, +}: CompilationTemplateCardProps) { + const { t } = useTranslation(); + const kinds = useMemo( + () => Array.from(new Set((data.templates ?? []).map((item) => item.kind))), + [data.templates], + ); + + return ( + + + + +
+
+
+

+ {data.name} +

+ +
+ + + + +
+ +

+ {data.description} +

+ +
+ {kinds.map((kind) => ( + + {formatKindLabel(t, kind)} + + ))} +
+
+
+
+ ); +} diff --git a/web/src/pages/agents/compilation-template-dropdown.tsx b/web/src/pages/agents/compilation-template-dropdown.tsx new file mode 100644 index 0000000000..80e48c9400 --- /dev/null +++ b/web/src/pages/agents/compilation-template-dropdown.tsx @@ -0,0 +1,58 @@ +import { + ConfirmDeleteDialog, + ConfirmDeleteDialogNode, +} from '@/components/confirm-delete-dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template'; +import { Trash2 } from 'lucide-react'; +import { PropsWithChildren, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type CompilationTemplateDropdownProps = PropsWithChildren<{ + data: ICompilationTemplateGroup; + onDelete: (id: string) => void; +}>; + +export function CompilationTemplateDropdown({ + children, + data, + onDelete, +}: CompilationTemplateDropdownProps) { + const { t } = useTranslation(); + + const handleDelete = useCallback(() => { + onDelete(data.id); + }, [data.id, onDelete]); + + return ( + + +
e.stopPropagation()}>{children}
+
+ + , + }} + onOk={handleDelete} + > + e.preventDefault()} + onClick={(e) => e.stopPropagation()} + > + {t('common.delete')} + + + + +
+ ); +} diff --git a/web/src/pages/agents/constant.ts b/web/src/pages/agents/constant.ts index f0a6b3bbce..cb0c2bb108 100644 --- a/web/src/pages/agents/constant.ts +++ b/web/src/pages/agents/constant.ts @@ -1,4 +1,5 @@ export enum FlowType { Agent = 'agent', + Compiler = 'compiler', Flow = 'flow', } diff --git a/web/src/pages/agents/create-agent-dialog.tsx b/web/src/pages/agents/create-agent-dialog.tsx index 5881315a60..db35889217 100644 --- a/web/src/pages/agents/create-agent-dialog.tsx +++ b/web/src/pages/agents/create-agent-dialog.tsx @@ -1,46 +1,55 @@ -import { ButtonLoading } from '@/components/ui/button'; import { Dialog, DialogContent, - DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { TagRenameId } from '@/constants/knowledge'; +import { AgentCategory } from '@/constants/agent'; +import { BrainCircuit, Route } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { CreateAgentForm, CreateAgentFormProps } from './create-agent-form'; -type CreateAgentDialogProps = CreateAgentFormProps; +type CreateAgentDialogProps = CreateAgentFormProps & { + canvasCategory?: AgentCategory; +}; export function CreateAgentDialog({ hideModal, onOk, loading, - shouldChooseAgent, + canvasCategory, }: CreateAgentDialogProps) { const { t } = useTranslation(); + const dialogTitle = + canvasCategory === AgentCategory.DataflowCanvas + ? t('flow.createIngestionPipeline') + : canvasCategory === AgentCategory.AgentCanvas + ? t('flow.createWorkflow') + : t('common.create'); + + const DialogIcon = + canvasCategory === AgentCategory.DataflowCanvas + ? Route + : canvasCategory === AgentCategory.AgentCanvas + ? BrainCircuit + : undefined; + return ( - + - {t('flow.createGraph')} + + {DialogIcon && } + {dialogTitle} + - - - {t('common.save')} - - ); diff --git a/web/src/pages/agents/create-agent-form.tsx b/web/src/pages/agents/create-agent-form.tsx index ec36086d8a..731bc6cda1 100644 --- a/web/src/pages/agents/create-agent-form.tsx +++ b/web/src/pages/agents/create-agent-form.tsx @@ -1,29 +1,47 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Button, ButtonLoading } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; +import { DialogFooter } from '@/components/ui/dialog'; import { Form } from '@/components/ui/form'; import { TagRenameId } from '@/constants/knowledge'; import { IModalProps } from '@/interfaces/common'; import { cn } from '@/lib/utils'; -import { BrainCircuit, Check, Route } from 'lucide-react'; +import { Routes } from '@/routes'; +import { BrainCircuit, Check, Route, Shapes } from 'lucide-react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router'; import { FlowType } from './constant'; import { NameFormField, NameFormSchema } from './name-form-field'; export type CreateAgentFormProps = IModalProps & { - shouldChooseAgent?: boolean; + loading?: boolean; + showTypeCards?: boolean; }; type FlowTypeCardProps = { value?: FlowType; onChange?: (value: FlowType) => void; }; + +const FLOW_TYPE_CONFIG: Record< + FlowType, + { icon: typeof BrainCircuit; labelKey: string } +> = { + [FlowType.Flow]: { icon: Route, labelKey: 'tabList.ingestionPipeline' }, + [FlowType.Compiler]: { + icon: Shapes, + labelKey: 'tabList.compilationOperator', + }, + [FlowType.Agent]: { icon: BrainCircuit, labelKey: 'tabList.workflow' }, +}; + function FlowTypeCards({ value, onChange }: FlowTypeCardProps) { const { t } = useTranslation(); const handleChange = useCallback( @@ -35,8 +53,10 @@ function FlowTypeCards({ value, onChange }: FlowTypeCardProps) { return (
- {Object.values(FlowType).map((val) => { + {[FlowType.Flow, FlowType.Compiler, FlowType.Agent].map((val) => { const isActive = value === val; + const config = FLOW_TYPE_CONFIG[val]; + const Icon = config.icon; return (
- {val === FlowType.Agent ? ( - - ) : ( - - )} -

- {t( - `flow.${val === FlowType.Agent ? 'createAgent' : 'createPipeline'}`, - )} -

+ +

{t(`flow.${config.labelKey}`)}

{isActive && } @@ -87,15 +99,26 @@ export type FormSchemaType = z.infer; export function CreateAgentForm({ hideModal, onOk, - shouldChooseAgent = false, + loading, + showTypeCards = false, }: CreateAgentFormProps) { const { t } = useTranslation(); + const navigate = useNavigate(); const form = useForm({ resolver: zodResolver(FormSchema), defaultValues: { name: '', type: FlowType.Agent }, }); + const selectedType = useWatch({ control: form.control, name: 'type' }); + // Compilation operators are configured on the edit-next page, so the dialog + // skips the name field and turns the submit button into a navigation step. + const isCompiler = showTypeCards && selectedType === FlowType.Compiler; + + const handleNext = useCallback(() => { + navigate(`${Routes.CompilationTemplatesEditNext}?source=agents`); + }, [navigate]); + async function onSubmit(data: FormSchemaType) { const ret = await onOk?.(data); if (ret) { @@ -110,7 +133,7 @@ export function CreateAgentForm({ className="space-y-6" id={TagRenameId} > - {shouldChooseAgent && ( + {showTypeCards && ( )} - + {!isCompiler && } + + + {isCompiler ? ( + + ) : ( + + {t('common.confirm')} + + )} + ); } diff --git a/web/src/pages/agents/hooks/use-select-filters.ts b/web/src/pages/agents/hooks/use-select-filters.ts index 07d87b2365..1bbc6b4ed7 100644 --- a/web/src/pages/agents/hooks/use-select-filters.ts +++ b/web/src/pages/agents/hooks/use-select-filters.ts @@ -1,9 +1,11 @@ import { FilterCollection } from '@/components/list-filter-bar/interface'; +import { AgentCategory } from '@/constants/agent'; import { useFetchAgentList, useFetchAgentTags, } from '@/hooks/use-agent-request'; -import { buildOwnersFilter, groupListByType } from '@/utils/list-filter-util'; +import { AgentListItemType, IFlow } from '@/interfaces/database/agent'; +import { buildOwnersFilter } from '@/utils/list-filter-util'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,11 +14,14 @@ export function useSelectFilters() { const { data } = useFetchAgentList({}); const { data: tagCounts } = useFetchAgentTags(); - const canvasCategory = useMemo(() => { - return groupListByType( - data?.canvas ?? [], - 'canvas_category', - 'canvas_category', + // The merged /agents list also contains compilation template groups, which + // have no owner fields — drop them before building the owner filter. + const agents = useMemo(() => { + const canvas = (data?.canvas ?? []) as Array< + IFlow & { type?: AgentListItemType } + >; + return canvas.filter( + (x) => x.type !== AgentListItemType.CompilationTemplateGroup, ); }, [data?.canvas]); @@ -31,10 +36,23 @@ export function useSelectFilters() { ); const filters: FilterCollection[] = [ - buildOwnersFilter(data?.canvas ?? [], undefined, t('common.owner')), + buildOwnersFilter(agents, undefined, t('common.owner')), { field: 'canvasCategory', - list: canvasCategory, + list: [ + { + id: AgentCategory.DataflowCanvas, + label: t('flow.tabList.ingestionPipeline'), + }, + { + id: AgentListItemType.CompilationTemplateGroup, + label: t('flow.tabList.compilationOperator'), + }, + { + id: AgentCategory.AgentCanvas, + label: t('flow.tabList.workflow'), + }, + ], label: t('flow.canvasCategory'), }, { diff --git a/web/src/pages/agents/index.tsx b/web/src/pages/agents/index.tsx index 9bc8b9454d..6338a6845b 100644 --- a/web/src/pages/agents/index.tsx +++ b/web/src/pages/agents/index.tsx @@ -13,13 +13,15 @@ import { import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchAgentListByPage } from '@/hooks/use-agent-request'; +import { useDeleteCompilationTemplateGroup } from '@/hooks/use-compilation-template-group-request'; import { Routes } from '@/routes'; -import { t } from 'i18next'; import { pick } from 'lodash'; import { Clipboard, ClipboardPlus, FileInput, Plus } from 'lucide-react'; -import { useCallback, useEffect } from 'react'; -import { useSearchParams } from 'react-router'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useSearchParams } from 'react-router'; import { AgentCard } from './agent-card'; +import { CompilationTemplateCard } from './compilation-template-card'; import { CreateAgentDialog } from './create-agent-dialog'; import { useCreateAgentOrPipeline } from './hooks/use-create-agent'; import { useSelectFilters } from './hooks/use-select-filters'; @@ -27,7 +29,11 @@ import { UploadAgentDialog } from './upload-agent-dialog'; import { useHandleImportJsonFile } from './use-import-json'; import { useRenameAgent } from './use-rename-agent'; +const CompilationGroupCategory = 'compilation_template_group'; + export default function Agents() { + const { t } = useTranslation(); + const { data, loading: listLoading, @@ -39,7 +45,17 @@ export default function Agents() { handleFilterSubmit, } = useFetchAgentListByPage(); + const canvasCategory = useMemo( + () => + Array.isArray(filterValue.canvasCategory) + ? (filterValue.canvasCategory[0] as string | undefined) + : undefined, + [filterValue.canvasCategory], + ); + const isCompilation = canvasCategory === CompilationGroupCategory; + const { navigateToAgentTemplates } = useNavigatePage(); + const navigate = useNavigate(); const { agentRenameLoading, @@ -65,6 +81,8 @@ export default function Agents() { hideFileUploadModal, } = useHandleImportJsonFile(); + const { deleteGroup } = useDeleteCompilationTemplateGroup(); + const filters = useSelectFilters(); const handlePageChange = useCallback( @@ -73,6 +91,25 @@ export default function Agents() { }, [setPagination], ); + + const handleAddCompilation = useCallback(() => { + navigate(`${Routes.CompilationTemplatesEditNext}?source=agents`); + }, [navigate]); + + const handleEditCompilation = useCallback( + (id: string) => () => { + navigate(`${Routes.CompilationTemplatesEditNext}/${id}?source=agents`); + }, + [navigate], + ); + + const handleDeleteCompilation = useCallback( + async (id: string) => { + await deleteGroup(id); + }, + [deleteGroup], + ); + const [searchUrl, setSearchUrl] = useSearchParams(); const isCreate = searchUrl.get('isCreate') === 'true'; @@ -86,26 +123,32 @@ export default function Agents() { return ( <> - {data?.length || searchString ? ( -
-
- +
+
+ + {isCompilation ? ( + + ) : ( @@ -133,93 +176,99 @@ export default function Agents() { - -
+ )} + +
- {data.length ? ( - <> - - {data.map((x) => { - return ( + {data.length ? ( + <> + + {isCompilation + ? (data as any[]).map((item) => ( + + )) + : data.map((x) => ( - ); - })} - + ))} + -
- -
- - ) : ( -
- showCreatingModal()} +
+ -
- )} -
- ) : listLoading ? ( -
- ) : ( -
- showCreatingModal()} - > -
    -
  • - -
  • + + + ) : searchString ? ( +
    + showCreatingModal()} + /> +
    + ) : listLoading ? null : ( +
    + showCreatingModal()} + > +
      +
    • + +
    • -
    • - -
    • +
    • + +
    • -
    • - -
    • -
    -
    -
- )} +
  • + +
  • + + + + )} + {agentRenameVisible && ( )} diff --git a/web/src/pages/chunk/representation/components/representation-select.tsx b/web/src/pages/chunk/representation/components/representation-select.tsx index 380e4b87f0..824059c840 100644 --- a/web/src/pages/chunk/representation/components/representation-select.tsx +++ b/web/src/pages/chunk/representation/components/representation-select.tsx @@ -5,6 +5,7 @@ import { import { type IStructureGraphTemplate } from '@/interfaces/database/document-structure'; import { formatKindLabel } from '@/utils/compilation-template-util'; import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; interface RepresentationSelectProps { templates: IStructureGraphTemplate[]; @@ -17,6 +18,7 @@ export function RepresentationSelect({ value, onChange, }: RepresentationSelectProps) { + const { t } = useTranslation(); const options = useMemo(() => { return templates.map((template) => ({ value: template.template_id, @@ -24,13 +26,13 @@ export function RepresentationSelect({ {template.template_name} - {formatKindLabel(template.kind)} + {formatKindLabel(t, template.kind)} ), keywords: [template.template_name, template.kind], })); - }, [templates]); + }, [templates, t]); return ( void, +) { + const [graphKeywords, setGraphKeywords] = useState(''); + const [selectedNodeId, setSelectedNodeId] = useState(''); // entity name + + const { data, loading } = useFetchDocumentStructureGraph(graphKeywords); + const templates = useMemo(() => data?.templates ?? [], [data?.templates]); + const { + selectedTemplateId, + setSelectedTemplateId, + selectedTemplate, + selectedKind, + } = useSelectedTemplate(templates); + const isGraphKind = selectedKind === CompilationTemplateKind.KnowledgeGraph; + + const entityOptions = useMemo( + () => + (selectedTemplate?.entities ?? []).map((entity) => { + const name = getEntityDisplayName(entity); + return { + label: name, + value: name, + keywords: [name, ...(entity.aliases ?? [])], + }; + }), + [selectedTemplate?.entities], + ); + + // Only refill the select when the selected entity is still in the current + // graph data, to avoid showing raw text with no matching option + const selectedEntityName = + selectedNodeId && + (selectedTemplate?.entities ?? []).some( + (entity) => getEntityDisplayName(entity) === selectedNodeId, + ) + ? selectedNodeId + : ''; + + const handleSelectEntity = useCallback( + (name: string) => { + if (!name) { + setGraphKeywords(''); + setSelectedNodeId(''); + return; + } + setSelectedNodeId(name); + const entity = (selectedTemplate?.entities ?? []).find( + (item) => getEntityDisplayName(item) === name, + ); + if (entity?.source_chunk_ids?.length) { + onNodeClick?.({ + id: entity.id ?? name, + name, + source_chunk_ids: entity.source_chunk_ids, + }); + } + }, + [selectedTemplate?.entities, onNodeClick], + ); + + const handleNoMatchEnter = useCallback((keywords: string) => { + setGraphKeywords(keywords); + setSelectedNodeId(''); + }, []); + + // Two-way binding: clicking a graph node selects it in the dropdown, + // then forwards to chunk navigation like before + const handleNodeClick = useCallback( + (node: ClickableNode) => { + if (isGraphKind && node.name) { + setSelectedNodeId(node.name); + } + if (!node.source_chunk_ids?.length) return; + onNodeClick?.(node); + }, + [isGraphKind, onNodeClick], + ); + + const handleTemplateChange = useCallback( + (templateId: string) => { + setSelectedTemplateId(templateId); + setGraphKeywords(''); + setSelectedNodeId(''); + }, + [setSelectedTemplateId], + ); + + return { + data, + loading, + templates, + selectedTemplateId, + selectedTemplate, + isGraphKind, + entityOptions, + graphSelectValue: selectedEntityName || graphKeywords, + highlightNodeId: selectedEntityName || null, + handleSelectEntity, + handleNoMatchEnter, + handleTemplateChange, + handleNodeClick, + }; +} diff --git a/web/src/pages/chunk/representation/index.tsx b/web/src/pages/chunk/representation/index.tsx index 6932b7ea4a..a69cf193de 100644 --- a/web/src/pages/chunk/representation/index.tsx +++ b/web/src/pages/chunk/representation/index.tsx @@ -1,19 +1,18 @@ import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; import { ExpandableSearchInput } from '@/components/expandable-search-input'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { SkeletonCard } from '@/components/skeleton-card'; import { Button } from '@/components/ui/button'; -import { - useDeleteDocumentStructureGraph, - useFetchDocumentStructureGraph, -} from '@/hooks/use-document-request'; +import { useDeleteDocumentStructureGraph } from '@/hooks/use-document-request'; import { Trash2 } from 'lucide-react'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { type ClickableNode, RepresentationRenderer, -} from './components/representation-renderer'; +} from '@/components/structure-graph/representation-renderer'; import { RepresentationSelect } from './components/representation-select'; -import { useSelectedTemplate } from './hooks/use-selected-template'; +import { useGraphEntitySearch } from './hooks/use-graph-entity-search'; interface RepresentationProps { onNodeClick?: (node: ClickableNode) => void; @@ -21,14 +20,26 @@ interface RepresentationProps { function Representation({ onNodeClick }: RepresentationProps) { const { t } = useTranslation(); - const { data, loading } = useFetchDocumentStructureGraph(); const { deleteDocumentStructureGraph, loading: deleting } = useDeleteDocumentStructureGraph(); - const templates = data?.templates ?? []; - const { selectedTemplateId, setSelectedTemplateId, selectedTemplate } = - useSelectedTemplate(templates); const [searchKeyword, setSearchKeyword] = useState(''); + const { + data, + loading, + templates, + selectedTemplateId, + selectedTemplate, + isGraphKind, + entityOptions, + graphSelectValue, + highlightNodeId, + handleSelectEntity, + handleNoMatchEnter, + handleTemplateChange, + handleNodeClick, + } = useGraphEntitySearch(onNodeClick); + const handleSearchChange = useCallback((value: string) => { setSearchKeyword(value); }, []); @@ -38,28 +49,32 @@ function Representation({ onNodeClick }: RepresentationProps) { await deleteDocumentStructureGraph(selectedTemplateId); }, [deleteDocumentStructureGraph, selectedTemplateId]); - const handleNodeClick = useCallback( - (node: ClickableNode) => { - if (!node.source_chunk_ids?.length) return; - onNodeClick?.(node); - }, - [onNodeClick], - ); - return (
    - + {isGraphKind ? ( + + ) : ( + + )} {templates.length > 0 && (
    - {loading && ( -
    - {t('common.loading', 'Loading...')} -
    - )} - {!loading && templates.length === 0 && ( + {loading && !data && } + {!(loading && !data) && templates.length === 0 && (
    {t( 'chunk.representationEmpty', @@ -89,11 +100,12 @@ function Representation({ onNodeClick }: RepresentationProps) { )}
    )} - {!loading && templates.length > 0 && ( + {!(loading && !data) && templates.length > 0 && ( )}
    diff --git a/web/src/pages/dataset/compilation/constants.ts b/web/src/pages/dataset/compilation/constants.ts index 72c32704b9..f059292c0e 100644 --- a/web/src/pages/dataset/compilation/constants.ts +++ b/web/src/pages/dataset/compilation/constants.ts @@ -1,10 +1,40 @@ +import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants'; + export enum ViewMode { LlmWiki = 'llm-wiki', Skills = 'skills', Tree = 'tree', + Graph = 'graph', + MindMap = 'mindmap', + Timeline = 'timeline', + // SessionEssence = 'session_essence', + // SessionGraph = 'session_graph', } export enum LeftPanelTab { Contents = 'contents', Graph = 'graph', } + +export const StructureKinds = [ + ViewMode.Graph, + ViewMode.MindMap, + ViewMode.Timeline, + // ViewMode.SessionEssence, + // ViewMode.SessionGraph, +] as const; + +export type StructureKind = (typeof StructureKinds)[number]; + +export type GenerableViewMode = Exclude; + +export const ViewModeGenerateTypeMap: Record = + { + [ViewMode.LlmWiki]: GenerateType.Artifact, + [ViewMode.Skills]: GenerateType.ToSkills, + [ViewMode.Graph]: GenerateType.KnowledgeGraph, + [ViewMode.MindMap]: GenerateType.MindMap, + [ViewMode.Timeline]: GenerateType.Timeline, + // [ViewMode.SessionEssence]: GenerateType.SessionEssence, + // [ViewMode.SessionGraph]: GenerateType.SessionGraph, + }; diff --git a/web/src/pages/dataset/compilation/dataset-structure-view.tsx b/web/src/pages/dataset/compilation/dataset-structure-view.tsx new file mode 100644 index 0000000000..870249ee31 --- /dev/null +++ b/web/src/pages/dataset/compilation/dataset-structure-view.tsx @@ -0,0 +1,127 @@ +import { + SelectWithSearch, + SelectWithSearchFlagOptionType, +} from '@/components/originui/select-with-search'; +import { getEntityDisplayName } from '@/components/structure-graph/adapters'; +import { RepresentationRenderer } from '@/components/structure-graph/representation-renderer'; +import { Card } from '@/components/ui/card'; +import { + DatasetStructureKeys, + useFetchDatasetStructureGraph, + useFetchKnowledgeBaseConfiguration, + useKnowledgeBaseId, +} from '@/hooks/use-knowledge-request'; +import { GenerateStatus } 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, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { StructureKind, ViewMode, ViewModeGenerateTypeMap } from './constants'; +import CompilationEmptyState from './empty-state'; +import { CompilationLoadingCard } from './loading-card'; + +interface DatasetStructureViewProps { + kind: StructureKind; +} + +export function DatasetStructureView({ kind }: DatasetStructureViewProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const knowledgeBaseId = useKnowledgeBaseId(); + const { data: knowledgeBase } = useFetchKnowledgeBaseConfiguration(); + const [graphKeywords, setGraphKeywords] = useState(''); + const [selectedNodeId, setSelectedNodeId] = useState(''); + const { data, loading } = useFetchDatasetStructureGraph(kind, graphKeywords); + const template = data?.templates?.[0]; + + const { data: structureRunData } = useTraceRunData( + ViewModeGenerateTypeMap[kind], + ); + const { status: structureStatus } = useGenerateStatus(structureRunData); + + useEffect(() => { + if (structureStatus === GenerateStatus.completed) { + queryClient.invalidateQueries({ + queryKey: DatasetStructureKeys.kind(knowledgeBaseId, kind), + }); + } + }, [structureStatus, queryClient, knowledgeBaseId, kind]); + + const entityOptions = useMemo( + () => + (template?.entities ?? []).map((entity) => { + const name = getEntityDisplayName(entity); + return { + label: name, + value: name, + keywords: [name, ...(entity.aliases ?? [])], + }; + }), + [template?.entities], + ); + + // Only refill the select when the selected entity is still in the current + // graph data, to avoid showing raw text with no matching option + const selectedEntityName = + selectedNodeId && + (template?.entities ?? []).some( + (entity) => getEntityDisplayName(entity) === selectedNodeId, + ) + ? selectedNodeId + : ''; + + const handleSelectEntity = useCallback((name: string) => { + if (!name) { + setGraphKeywords(''); + setSelectedNodeId(''); + return; + } + setSelectedNodeId(name); + }, []); + + const handleNoMatchEnter = useCallback((keywords: string) => { + setGraphKeywords(keywords); + setSelectedNodeId(''); + }, []); + + const canGenerate = (knowledgeBase?.chunk_count ?? 0) > 0; + + if (loading && !data) { + return ; + } + + if (!template && !graphKeywords) { + return ( + + ); + } + + return ( + + {kind === ViewMode.Graph && ( +
    + +
    + )} + +
    + ); +} diff --git a/web/src/pages/dataset/compilation/empty-state.tsx b/web/src/pages/dataset/compilation/empty-state.tsx index 032d355413..dc4a2b97f7 100644 --- a/web/src/pages/dataset/compilation/empty-state.tsx +++ b/web/src/pages/dataset/compilation/empty-state.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'; import { IconFontFill } from '@/components/icon-font'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; -import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants'; import { ITraceInfo, useDatasetGenerate, @@ -14,7 +13,13 @@ import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-g import { replaceText } from '@/pages/dataset/process-log-modal'; import { toFixed } from '@/utils/common-util'; -type EmptyStateType = 'llm-wiki' | 'skills'; +import { + GenerableViewMode, + ViewMode, + ViewModeGenerateTypeMap, +} from './constants'; + +type EmptyStateType = GenerableViewMode; interface ICompilationEmptyStateProps { type: EmptyStateType; @@ -22,19 +27,24 @@ interface ICompilationEmptyStateProps { data?: ITraceInfo; } -const DefaultGenerateTypeMap: Record = { - 'llm-wiki': GenerateType.Artifact, - skills: GenerateType.ToSkills, -}; - const TitleKeyMap: Record = { - 'llm-wiki': 'knowledgeDetails.noWikiPages', - skills: 'knowledgeDetails.noSkills', + [ViewMode.LlmWiki]: 'knowledgeDetails.noWikiPages', + [ViewMode.Skills]: 'knowledgeDetails.noSkills', + [ViewMode.Graph]: 'knowledgeDetails.noStructureGraph', + [ViewMode.MindMap]: 'knowledgeDetails.noStructureMindmap', + [ViewMode.Timeline]: 'knowledgeDetails.noStructureTimeline', + // [ViewMode.SessionEssence]: 'knowledgeDetails.noStructureSessionEssence', + // [ViewMode.SessionGraph]: 'knowledgeDetails.noStructureSessionGraph', }; const LabelKeyMap: Record = { - 'llm-wiki': 'knowledgeDetails.artifact', - skills: 'knowledgeDetails.toSkills', + [ViewMode.LlmWiki]: 'knowledgeDetails.artifact', + [ViewMode.Skills]: 'knowledgeDetails.toSkills', + [ViewMode.Graph]: 'knowledgeDetails.structureGraph', + [ViewMode.MindMap]: 'knowledgeDetails.structureMindmap', + [ViewMode.Timeline]: 'knowledgeDetails.structureTimeline', + // [ViewMode.SessionEssence]: 'knowledgeDetails.structureSessionEssence', + // [ViewMode.SessionGraph]: 'knowledgeDetails.structureSessionGraph', }; export function CompilationEmptyState({ @@ -43,7 +53,7 @@ export function CompilationEmptyState({ data, }: ICompilationEmptyStateProps) { const { t } = useTranslation(); - const generateType = DefaultGenerateTypeMap[type]; + const generateType = ViewModeGenerateTypeMap[type]; const { runGenerate, pauseGenerate } = useDatasetGenerate(); const { status, percent } = useGenerateStatus(data); diff --git a/web/src/pages/dataset/compilation/index.tsx b/web/src/pages/dataset/compilation/index.tsx index d6378cfd4d..edb919e2d9 100644 --- a/web/src/pages/dataset/compilation/index.tsx +++ b/web/src/pages/dataset/compilation/index.tsx @@ -1,13 +1,17 @@ import BackButton from '@/components/back-button'; +import { + SelectWithSearch, + type SelectWithSearchFlagOptionType, +} from '@/components/originui/select-with-search'; import { RAGFlowAvatar } from '@/components/ragflow-avatar'; -import { Button } from '@/components/ui/button'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; -import { ViewMode } from './constants'; +import { StructureKinds, ViewMode } from './constants'; +import { DatasetStructureView } from './dataset-structure-view'; import { LlmWikiView } from './llm-wiki-view'; import { NavTreeView } from './nav-tree-view'; import { SkillsView } from './skills-view'; @@ -19,17 +23,48 @@ export default function Compilation() { const { data: knowledgeBase } = useFetchKnowledgeBaseConfiguration(); const [viewMode, setViewMode] = useState(ViewMode.LlmWiki); - const handleSwitchToLlmWiki = useCallback(() => { - setViewMode(ViewMode.LlmWiki); + const viewOptions = useMemo(() => { + return [ + { + value: ViewMode.LlmWiki, + label: t('knowledgeDetails.llmWiki'), + }, + { + value: ViewMode.Skills, + label: t('knowledgeDetails.skills', 'To Skills'), + }, + { + value: ViewMode.Tree, + label: t('knowledgeDetails.navTree'), + }, + { + value: ViewMode.Graph, + label: t('knowledgeDetails.structureGraph'), + }, + { + value: ViewMode.MindMap, + label: t('knowledgeDetails.structureMindmap'), + }, + { + value: ViewMode.Timeline, + label: t('knowledgeDetails.structureTimeline'), + }, + // { + // value: ViewMode.SessionEssence, + // label: t('knowledgeDetails.structureSessionEssence'), + // }, + // { + // value: ViewMode.SessionGraph, + // label: t('knowledgeDetails.structureSessionGraph'), + // }, + ]; + }, [t]); + + const handleViewModeChange = useCallback((value: string) => { + setViewMode(value as ViewMode); }, []); - const handleSwitchToSkills = useCallback(() => { - setViewMode(ViewMode.Skills); - }, []); - - const handleSwitchToTree = useCallback(() => { - setViewMode(ViewMode.Tree); - }, []); + const structureKind = StructureKinds.find((kind) => kind === viewMode); return (
    @@ -51,37 +86,19 @@ export default function Compilation() { -
    - - - - - -
    +
    {viewMode === ViewMode.LlmWiki && } {viewMode === ViewMode.Skills && } {viewMode === ViewMode.Tree && } + {structureKind && }
    ); } diff --git a/web/src/pages/dataset/compilation/llm-wiki-view.tsx b/web/src/pages/dataset/compilation/llm-wiki-view.tsx index 9c0ed739f8..190cfba43d 100644 --- a/web/src/pages/dataset/compilation/llm-wiki-view.tsx +++ b/web/src/pages/dataset/compilation/llm-wiki-view.tsx @@ -20,7 +20,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router'; -import { LeftPanelTab } from './constants'; +import { LeftPanelTab, ViewMode } from './constants'; import CompilationEmptyState from './empty-state'; import { useCompilationArtifact } from './hooks/use-compilation-artifact'; import { CompilationLoadingCard } from './loading-card'; @@ -43,9 +43,11 @@ export function LlmWikiView() { const { data: artifactRunData } = useTraceRunData(GenerateType.Artifact); const { status: artifactStatus } = useGenerateStatus(artifactRunData); + const [updateSheetOpen, setUpdateSheetOpen] = useState(false); useEffect(() => { if (artifactStatus === GenerateStatus.completed) { + setUpdateSheetOpen(false); queryClient.invalidateQueries({ queryKey: ArtifactKeys.listByDataset(id!), }); @@ -70,7 +72,7 @@ export function LlmWikiView() { if (isEmpty) { return ( @@ -88,6 +90,9 @@ export function LlmWikiView() { onSelectArtifact={handleSelectArtifact} onClearArtifact={clearSelectedArtifact} onClearWiki={clearSelectedArtifact} + updateSheetOpen={updateSheetOpen} + onUpdateSheetOpenChange={setUpdateSheetOpen} + traceData={artifactRunData} /> diff --git a/web/src/pages/dataset/compilation/skills-view.tsx b/web/src/pages/dataset/compilation/skills-view.tsx index a4029aab03..02262e106a 100644 --- a/web/src/pages/dataset/compilation/skills-view.tsx +++ b/web/src/pages/dataset/compilation/skills-view.tsx @@ -17,6 +17,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; import { useParams } from 'react-router'; +import { ViewMode } from './constants'; import CompilationEmptyState from './empty-state'; import { useCompilationSkill } from './hooks/use-compilation-skill'; import { CompilationLoadingCard } from './loading-card'; @@ -56,7 +57,7 @@ export function SkillsView() { if (isEmpty) { return ( diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-update.ts b/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-update.ts new file mode 100644 index 0000000000..62ff99c095 --- /dev/null +++ b/web/src/pages/dataset/compilation/wiki-left-panel/hooks/use-wiki-update.ts @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; +import { + useFetchArtifactAlteration, + useRunArtifactIndex, +} from '@/hooks/use-knowledge-request'; + +type UseWikiUpdateOptions = { + onUpdate?: () => void; +}; + +export function useWikiUpdate({ onUpdate }: UseWikiUpdateOptions = {}) { + const { data, loading: queryLoading } = useFetchArtifactAlteration(); + const { runArtifactIndex, loading: mutationLoading } = useRunArtifactIndex(); + + const newlyUploaded = data?.newly_uploaded ?? 0; + const removed = data?.removed ?? 0; + const hasChanges = newlyUploaded > 0 || removed > 0; + + const handleUpdate = useCallback(async () => { + const result = await runArtifactIndex(); + if (result?.code === 0) { + onUpdate?.(); + } + }, [runArtifactIndex, onUpdate]); + + return { + hasChanges, + newlyUploaded, + removed, + handleUpdate, + loading: queryLoading || mutationLoading, + }; +} diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx index ddb8f11e0e..6639cc2862 100644 --- a/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx +++ b/web/src/pages/dataset/compilation/wiki-left-panel/index.tsx @@ -1,26 +1,25 @@ -import ArtifactForceGraph from '@/components/artifact-force-graph'; import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; -import { - SelectWithSearch, - SelectWithSearchFlagOptionType, -} from '@/components/originui/select-with-search'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { useFetchArtifactGraph } from '@/hooks/use-knowledge-request'; -import { IArtifact, IArtifactGraphEntity } from '@/interfaces/database/dataset'; -import { Trash2 } from 'lucide-react'; -import { useCallback, useMemo } from 'react'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { IArtifact } from '@/interfaces/database/dataset'; +import { ITraceInfo } from '@/pages/dataset/dataset/generate-button/hook'; +import { Trash2, WandSparkles } from 'lucide-react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { LeftPanelTab } from '../constants'; import { useWikiClear } from './hooks/use-wiki-clear'; +import { useWikiUpdate } from './hooks/use-wiki-update'; +import { WikiGraphPanel } from './wiki-graph-panel'; import { WikiNavBar } from './wiki-nav-bar'; - -const mapNodeToValue = (node: IArtifactGraphEntity) => ({ - slug: node.slug, - title: node.name, - page_type: node.type, -}); +import { WikiUpdateSheet } from './wiki-update-sheet'; type WikiLeftPanelProps = { tab: LeftPanelTab; @@ -29,6 +28,9 @@ type WikiLeftPanelProps = { onSelectArtifact: (artifact: IArtifact) => void; onClearArtifact: () => void; onClearWiki?: () => void; + updateSheetOpen: boolean; + onUpdateSheetOpenChange: (open: boolean) => void; + traceData?: ITraceInfo; }; export function WikiLeftPanel({ @@ -38,59 +40,66 @@ export function WikiLeftPanel({ onSelectArtifact, onClearArtifact, onClearWiki, + updateSheetOpen, + onUpdateSheetOpenChange, + traceData, }: WikiLeftPanelProps) { const { t } = useTranslation(); - const { data } = useFetchArtifactGraph(undefined, { - enabled: tab === LeftPanelTab.Graph, - }); + const { open, setOpen, handleConfirm, loading } = useWikiClear({ onClearWiki, }); - const entityOptions = useMemo( - () => - data.entities.map((entity) => ({ - label: entity.name, - value: entity.slug, - keywords: [entity.name, ...entity.aliases], - })), - [data.entities], - ); + const { + hasChanges, + newlyUploaded, + removed, + handleUpdate, + loading: updateLoading, + } = useWikiUpdate(); - // Only refill the select when selectedArtifact is a graph entity, to avoid showing the raw slug - const selectedEntitySlug = data.entities.some( - (entity) => entity.slug === selectedArtifact?.slug, - ) - ? (selectedArtifact?.slug ?? '') - : ''; - - const handleSelectEntity = useCallback( - (slug: string) => { - if (!slug) { - onClearArtifact(); - return; - } - const entity = data.entities.find((item) => item.slug === slug); - if (entity) { - onSelectArtifact(mapNodeToValue(entity)); - } - }, - [data.entities, onSelectArtifact, onClearArtifact], - ); + const handleUpdateClick = useCallback(async () => { + onUpdateSheetOpenChange(true); + await handleUpdate(); + }, [handleUpdate, onUpdateSheetOpenChange]); return ( ); } diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-graph-panel.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-graph-panel.tsx new file mode 100644 index 0000000000..6b42e38cd3 --- /dev/null +++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-graph-panel.tsx @@ -0,0 +1,100 @@ +import ArtifactForceGraph from '@/components/artifact-force-graph'; +import { + SelectWithSearch, + SelectWithSearchFlagOptionType, +} from '@/components/originui/select-with-search'; +import { useFetchArtifactGraph } from '@/hooks/use-knowledge-request'; +import { IArtifact, IArtifactGraphEntity } from '@/interfaces/database/dataset'; +import { IFetchArtifactGraphRequestParams } from '@/interfaces/request/knowledge'; +import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const GraphSearchTopN = 12; + +const mapNodeToValue = (node: IArtifactGraphEntity) => ({ + slug: node.slug, + title: node.name, + page_type: node.type, +}); + +type WikiGraphPanelProps = { + selectedArtifact: IArtifact | null; + onSelectArtifact: (artifact: IArtifact) => void; + onClearArtifact: () => void; +}; + +export function WikiGraphPanel({ + selectedArtifact, + onSelectArtifact, + onClearArtifact, +}: WikiGraphPanelProps) { + const { t } = useTranslation(); + const [graphKeywords, setGraphKeywords] = useState(''); + + const graphParams = useMemo( + () => + graphKeywords + ? { keywords: graphKeywords, top_n: GraphSearchTopN } + : undefined, + [graphKeywords], + ); + const { data } = useFetchArtifactGraph(graphParams); + + const entityOptions = useMemo( + () => + data.entities.map((entity) => ({ + label: entity.name, + value: entity.slug, + keywords: [entity.name, ...entity.aliases], + })), + [data.entities], + ); + + // Only refill the select when selectedArtifact is a graph entity, to avoid showing the raw slug + const selectedEntitySlug = data.entities.some( + (entity) => entity.slug === selectedArtifact?.slug, + ) + ? (selectedArtifact?.slug ?? '') + : ''; + + const handleSelectEntity = useCallback( + (slug: string) => { + if (!slug) { + setGraphKeywords(''); + onClearArtifact(); + return; + } + const entity = data.entities.find((item) => item.slug === slug); + if (entity) { + onSelectArtifact(mapNodeToValue(entity)); + } + }, + [data.entities, onSelectArtifact, onClearArtifact], + ); + + const handleNoMatchEnter = useCallback((keywords: string) => { + setGraphKeywords(keywords); + }, []); + + return ( +
    + + +
    + ); +} diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx index 4befda5d2e..52e5a64f4c 100644 --- a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx +++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-nav-bar.tsx @@ -51,7 +51,7 @@ export function WikiNavBar({ } = useCreateDirectory(); return ( -
    +
    diff --git a/web/src/pages/dataset/compilation/wiki-left-panel/wiki-update-sheet.tsx b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-update-sheet.tsx new file mode 100644 index 0000000000..57998d4416 --- /dev/null +++ b/web/src/pages/dataset/compilation/wiki-left-panel/wiki-update-sheet.tsx @@ -0,0 +1,101 @@ +import { CirclePause, Logs } from 'lucide-react'; +import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { cn } from '@/lib/utils'; +import { GenerateType } from '@/pages/dataset/dataset/generate-button/constants'; +import { + ITraceInfo, + useDatasetGenerate, +} from '@/pages/dataset/dataset/generate-button/hook'; +import { useGenerateStatus } from '@/pages/dataset/dataset/generate-button/use-generate-status'; +import { replaceText } from '@/pages/dataset/process-log-modal'; +import { toFixed } from '@/utils/common-util'; + +type WikiUpdateSheetProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + data?: ITraceInfo; +}; + +export function WikiUpdateSheet({ + open, + onOpenChange, + data, +}: WikiUpdateSheetProps) { + const { t } = useTranslation(); + const { pauseGenerate } = useDatasetGenerate(); + const { status, percent } = useGenerateStatus(data); + + useEffect(() => { + if (status === 'completed') { + onOpenChange(false); + } + }, [status, onOpenChange]); + + const handlePause = useCallback(() => { + if (data?.id) { + pauseGenerate({ task_id: data.id, type: GenerateType.Artifact }).catch( + () => {}, + ); + } + }, [pauseGenerate, data?.id]); + + return ( + + + + + {t('knowledgeDetails.updateSheetTitle', { + defaultValue: 'Update Wiki', + })} + + +
    +
    + + + {t('knowledgeDetails.artifact', { defaultValue: 'Artifact' })} + +
    +
    +
    +
    +
    + {status === 'running' && ( + {(toFixed(percent) as string) + '%'} + )} + {status !== 'failed' && ( + + + + )} +
    +
    + {replaceText(data?.progress_msg || '')} +
    +
    + + + ); +} diff --git a/web/src/pages/dataset/dataset-overview/dataset-common.ts b/web/src/pages/dataset/dataset-overview/dataset-common.ts index aecc2225aa..afce1b1ec6 100644 --- a/web/src/pages/dataset/dataset-overview/dataset-common.ts +++ b/web/src/pages/dataset/dataset-overview/dataset-common.ts @@ -8,6 +8,10 @@ export enum ProcessingType { raptor = 'RAPTOR', artifact = 'Artifact', skill = 'Skill', + mindmap = 'Mindmap', + timeline = 'Timeline', + sessionEssence = 'Session_Essence', + sessionGraph = 'Session_Graph', } export const ProcessingTypeMap = { @@ -15,5 +19,9 @@ export const ProcessingTypeMap = { [ProcessingType.raptor]: 'RAPTOR', [ProcessingType.artifact]: 'Artifact', [ProcessingType.skill]: 'Skill', + [ProcessingType.mindmap]: 'Mind Map', + [ProcessingType.timeline]: 'Timeline', + [ProcessingType.sessionEssence]: 'Session Essence', + [ProcessingType.sessionGraph]: 'Session Graph', GraphRAG: 'Knowledge Graph', }; diff --git a/web/src/pages/dataset/dataset/generate-button/constants.ts b/web/src/pages/dataset/dataset/generate-button/constants.ts index e3da71b250..d1369fbe4f 100644 --- a/web/src/pages/dataset/dataset/generate-button/constants.ts +++ b/web/src/pages/dataset/dataset/generate-button/constants.ts @@ -12,6 +12,10 @@ export enum GenerateType { Raptor = 'Raptor', Artifact = 'Artifact', ToSkills = 'ToSkills', + MindMap = 'MindMap', + Timeline = 'Timeline', + SessionEssence = 'SessionEssence', + SessionGraph = 'SessionGraph', } export enum TraceType { @@ -19,6 +23,10 @@ export enum TraceType { Raptor = 'raptor', Artifact = 'artifact', Skill = 'skill', + MindMap = 'mindmap', + Timeline = 'timeline', + SessionEssence = 'session_essence', + SessionGraph = 'session_graph', } export const GenerateTypeMap = { @@ -26,4 +34,8 @@ export const GenerateTypeMap = { [GenerateType.Raptor]: ProcessingType.raptor, [GenerateType.Artifact]: ProcessingType.artifact, [GenerateType.ToSkills]: ProcessingType.skill, + [GenerateType.MindMap]: ProcessingType.mindmap, + [GenerateType.Timeline]: ProcessingType.timeline, + [GenerateType.SessionEssence]: ProcessingType.sessionEssence, + [GenerateType.SessionGraph]: ProcessingType.sessionGraph, }; diff --git a/web/src/pages/dataset/dataset/generate-button/hook.ts b/web/src/pages/dataset/dataset/generate-button/hook.ts index ae1e00c2e6..04cbd583e1 100644 --- a/web/src/pages/dataset/dataset/generate-button/hook.ts +++ b/web/src/pages/dataset/dataset/generate-button/hook.ts @@ -18,7 +18,7 @@ enum DatasetKey { const PollIntervalMs = 5000; -const DatasetGenerateKeys = { +export const DatasetGenerateKeys = { trace: (type: GenerateType, id?: string, open?: boolean) => [type, id, open] as const, traceById: (type: GenerateType, id?: string) => [type, id] as const, @@ -74,6 +74,10 @@ const TraceTypeMap: Record = { [GenerateType.Raptor]: TraceType.Raptor, [GenerateType.Artifact]: TraceType.Artifact, [GenerateType.ToSkills]: TraceType.Skill, + [GenerateType.MindMap]: TraceType.MindMap, + [GenerateType.Timeline]: TraceType.Timeline, + [GenerateType.SessionEssence]: TraceType.SessionEssence, + [GenerateType.SessionGraph]: TraceType.SessionGraph, }; export const useTraceRunData = (type: GenerateType) => { diff --git a/web/src/pages/next-search/markdown-content/index.tsx b/web/src/pages/next-search/markdown-content/index.tsx index 7858fc6edf..8b05ecd508 100644 --- a/web/src/pages/next-search/markdown-content/index.tsx +++ b/web/src/pages/next-search/markdown-content/index.tsx @@ -35,7 +35,7 @@ import { import { useFetchDocumentThumbnailsByIds } from '@/hooks/use-document-request'; import classNames from 'classnames'; import { omit } from 'lodash'; -import { pipe } from 'lodash/fp'; +import pipe from 'lodash/fp/pipe'; import reactStringReplace from 'react-string-replace'; // Defining Tailwind CSS class name constants diff --git a/web/src/pages/user-setting/compilation-templates/create-next/components/template-sidebar.tsx b/web/src/pages/user-setting/compilation-templates/create-next/components/template-sidebar.tsx index de2d1dbdab..c0c20f68a4 100644 --- a/web/src/pages/user-setting/compilation-templates/create-next/components/template-sidebar.tsx +++ b/web/src/pages/user-setting/compilation-templates/create-next/components/template-sidebar.tsx @@ -91,7 +91,7 @@ function TemplateSidebarItem({ {template?.kind && ( - {formatKindLabel(template.kind)} + {formatKindLabel(t, template.kind)} )}
    diff --git a/web/src/pages/user-setting/compilation-templates/create-next/hooks/use-create-next-compilation-template-group.ts b/web/src/pages/user-setting/compilation-templates/create-next/hooks/use-create-next-compilation-template-group.ts index 30fa195be9..c8d603cd2a 100644 --- a/web/src/pages/user-setting/compilation-templates/create-next/hooks/use-create-next-compilation-template-group.ts +++ b/web/src/pages/user-setting/compilation-templates/create-next/hooks/use-create-next-compilation-template-group.ts @@ -8,6 +8,7 @@ import { useFetchBuiltinCompilationTemplates } from '@/hooks/use-compilation-tem import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request'; import { isCreateCompilationTemplateGroup } from '@/utils/compilation-template-util'; import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { formatKindLabel } from '@/utils/compilation-template-util'; @@ -18,6 +19,7 @@ import { useCompilationTemplateGroupSubmit } from '@/pages/user-setting/compilat export const useCreateNextCompilationTemplateGroup = () => { const { id } = useParams<{ id: string }>(); const { navigateToCompilationTemplates } = useNavigatePage(); + const { t } = useTranslation(); const isCreate = isCreateCompilationTemplateGroup(id); @@ -35,9 +37,9 @@ export const useCreateNextCompilationTemplateGroup = () => { () => builtinKindOptions.map((option) => ({ ...option, - label: formatKindLabel(option.value), + label: formatKindLabel(t, option.value), })), - [builtinKindOptions], + [builtinKindOptions, t], ); const { form } = useCompilationTemplateGroupForm({ diff --git a/web/src/pages/user-setting/compilation-templates/edit-next/components/add-field-modal.tsx b/web/src/pages/user-setting/compilation-templates/edit-next/components/add-field-modal.tsx new file mode 100644 index 0000000000..be301e51df --- /dev/null +++ b/web/src/pages/user-setting/compilation-templates/edit-next/components/add-field-modal.tsx @@ -0,0 +1,118 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Button } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { Modal } from '@/components/ui/modal/modal'; +import { Textarea } from '@/components/ui/textarea'; +import { ICompilationTemplateSection } from '@/interfaces/database/compilation-template'; +import { startCase } from 'lodash'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FieldLabelKeyMap } from '../utils'; + +import { useAddFieldForm } from '../hooks/use-add-field-form'; + +type AddFieldModalProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + sectionName: string; + builtinSection?: ICompilationTemplateSection; + initialField?: Record; + onAdd: (field: Record) => void; +}; + +export function AddFieldModal({ + open, + onOpenChange, + sectionName, + builtinSection, + initialField, + onAdd, +}: AddFieldModalProps) { + const { t } = useTranslation(); + const { + form, + fieldKeys, + hasTypeField, + typeOptions, + handleTypeChange, + handleSubmit, + } = useAddFieldForm({ + open, + builtinSection, + initialField, + }); + + const nonTypeKeys = fieldKeys.filter((key) => key !== 'type'); + + const handleClose = useCallback(() => { + onOpenChange(false); + }, [onOpenChange]); + + const handleConfirm = useCallback( + (field: Record) => { + onAdd(field); + onOpenChange(false); + }, + [onAdd, onOpenChange], + ); + + const getFieldLabel = useCallback( + (key: string) => { + return FieldLabelKeyMap[key] ? t(FieldLabelKeyMap[key]) : startCase(key); + }, + [t], + ); + + return ( + + + +
    + } + > +
    +
    + {hasTypeField && ( + + {(field) => ( + { + field.onChange(value); + handleTypeChange(value); + }} + placeholder={t('setting.selectFieldType')} + allowCustomValue + /> + )} + + )} + + {nonTypeKeys.map((key) => ( + +