mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-05 15:20:30 +08:00
Feat: Search for knowledge-base-level graph nodes. (#17444)
This commit is contained in:
1
web/package-lock.json
generated
1
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<TNodeValue = IArtifactGraphEntity>({
|
||||
);
|
||||
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<TNodeValue = IArtifactGraphEntity>({
|
||||
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<TNodeValue = IArtifactGraphEntity>({
|
||||
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<TNodeValue = IArtifactGraphEntity>({
|
||||
[],
|
||||
);
|
||||
|
||||
// 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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('flex-1 min-h-0 h-full', !show && 'hidden')}
|
||||
>
|
||||
{dimensions.width > 0 && dimensions.height > 0 && (
|
||||
{hasDimensions && (
|
||||
<ForceGraph2D
|
||||
ref={fgRef}
|
||||
width={dimensions.width}
|
||||
@@ -109,7 +119,7 @@ function ArtifactForceGraph<TNodeValue = IArtifactGraphEntity>({
|
||||
nodeColor={nodeColor}
|
||||
nodeVal={nodeVal}
|
||||
cooldownTicks={100}
|
||||
nodeLabel={''}
|
||||
nodeLabel={getNodeLabel}
|
||||
autoPauseRedraw={false}
|
||||
onEngineStop={handleEngineStop}
|
||||
onNodeClick={handleNodeClick}
|
||||
@@ -118,6 +128,7 @@ function ArtifactForceGraph<TNodeValue = IArtifactGraphEntity>({
|
||||
nodeCanvasObjectMode={nodeCanvasObjectMode}
|
||||
linkColor={getLinkColor}
|
||||
linkWidth={getLinkWidth}
|
||||
linkLabel={getLinkLabel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export type ArtifactGraphNode = NodeObject<IArtifactGraphEntity> & {
|
||||
|
||||
export type ArtifactGraphLink = LinkObject<
|
||||
ArtifactGraphNode,
|
||||
{ source: string; target: string }
|
||||
{ source: string; target: string; type?: string }
|
||||
>;
|
||||
|
||||
export interface ArtifactForceGraphProps<TNodeValue = IArtifactGraphEntity> {
|
||||
|
||||
@@ -51,6 +51,7 @@ export const useArtifactGraphData = ({
|
||||
(relation) => ({
|
||||
source: relation.from,
|
||||
target: relation.to,
|
||||
type: relation.type,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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<ForceGraphMethods<ArtifactGraphNode> | undefined>,
|
||||
hasDimensions: boolean,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const fg = fgRef.current;
|
||||
if (!hasDimensions || !fg) return;
|
||||
fg.d3Force(
|
||||
'x',
|
||||
forceX<ArtifactGraphNode>(0).strength(CenterGravityStrength),
|
||||
);
|
||||
fg.d3Force(
|
||||
'y',
|
||||
forceY<ArtifactGraphNode>(0).strength(CenterGravityStrength),
|
||||
);
|
||||
}, [fgRef, hasDimensions]);
|
||||
}
|
||||
@@ -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<typeof ForceGraph2D>['nodeCanvasObject']
|
||||
>;
|
||||
|
||||
export const useGraphHighlight = (
|
||||
getBaseLinkColor: () => string,
|
||||
containerRef: React.RefObject<HTMLElement>,
|
||||
pinnedNode?: ArtifactGraphNode | null,
|
||||
) => {
|
||||
const [hoverNode, setHoverNode] = useState<ArtifactGraphNode | null>(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(
|
||||
|
||||
@@ -4,6 +4,16 @@ export const defaultMapNodeToValue = <TNode extends IArtifactGraphEntity>(
|
||||
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 (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<typeof Button>,
|
||||
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<HTMLInputElement>) => {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<CommandList className="mt-2 outline-none">
|
||||
|
||||
@@ -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 ?? '',
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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<IArtifactGraph>(
|
||||
() =>
|
||||
template
|
||||
? adaptKnowledgeGraphToForceGraph(template)
|
||||
: EmptyForceGraphData,
|
||||
[template],
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
@@ -143,10 +160,11 @@ export function RepresentationRenderer({
|
||||
return (
|
||||
<div className="mt-6 flex-1 min-h-0">
|
||||
<ArtifactForceGraph
|
||||
data={adaptKnowledgeGraphToForceGraph(template)}
|
||||
data={forceGraphData}
|
||||
show
|
||||
getNodeId={getArtifactNodeName}
|
||||
onNodeClick={handleArtifactNodeClick}
|
||||
highlightNodeId={highlightNodeId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -174,7 +192,7 @@ export function RepresentationRenderer({
|
||||
return (
|
||||
<div className="mt-6 flex-1 min-h-0">
|
||||
<ArtifactForceGraph
|
||||
data={adaptKnowledgeGraphToForceGraph(template)}
|
||||
data={forceGraphData}
|
||||
show
|
||||
getNodeId={getArtifactNodeName}
|
||||
onNodeClick={handleArtifactNodeClick}
|
||||
@@ -195,7 +213,7 @@ export function RepresentationRenderer({
|
||||
return (
|
||||
<div className="mt-6 flex-1 min-h-0">
|
||||
<ArtifactForceGraph
|
||||
data={adaptKnowledgeGraphToForceGraph(template)}
|
||||
data={forceGraphData}
|
||||
show
|
||||
getNodeId={getArtifactNodeName}
|
||||
onNodeClick={handleArtifactNodeClick}
|
||||
@@ -2,7 +2,7 @@ import { DagreLayout } from '@antv/layout';
|
||||
import { Graph, type EdgeMetadata, type NodeMetadata } from '@antv/x6';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { type TimelineX6NodeData } from '../../../utils/adapters';
|
||||
import { type TimelineX6NodeData } from '../../adapters';
|
||||
import { type TimelineNodeValue, type TimelineX6GraphProps } from '../types';
|
||||
|
||||
export function useX6Graph(
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
type TimelineX6EdgeData,
|
||||
type TimelineX6NodeData,
|
||||
} from '../../utils/adapters';
|
||||
import { type TimelineX6EdgeData, type TimelineX6NodeData } from '../adapters';
|
||||
|
||||
export interface TimelineNodeValue {
|
||||
id: string;
|
||||
@@ -223,6 +223,17 @@ export const useNavigatePage = () => {
|
||||
[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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<IStructureGraphResponse | null>({
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -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<IArtifactAlteration | null>({
|
||||
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<IArtifactGraph>({
|
||||
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<IStructureGraphResponse | null>({
|
||||
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<string, Record<string, string[]>>
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<string, IOperator>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface IFetchArtifactTopicListRequestParams {
|
||||
|
||||
export interface IFetchArtifactGraphRequestParams {
|
||||
node?: string;
|
||||
keywords?: string;
|
||||
top_n?: number;
|
||||
}
|
||||
|
||||
export interface IUpdateArtifactPageRequestBody {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<IRagNode>) {
|
||||
return (
|
||||
<RagNode {...props}>
|
||||
<section className="flex flex-col gap-2">
|
||||
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
|
||||
<LabelCard className="text-text-primary flex justify-between flex-col gap-1">
|
||||
<span className="text-text-secondary">
|
||||
{t('knowledgeConfiguration.compilationTemplate')}
|
||||
|
||||
@@ -362,7 +362,6 @@ export const initialExtractorValues = {
|
||||
};
|
||||
|
||||
export const initialCompilationValues = {
|
||||
...initialLlmBaseValues,
|
||||
compilation_template_group_ids: [],
|
||||
outputs: {
|
||||
chunks: { type: 'Array<Object>', value: [] },
|
||||
|
||||
@@ -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<typeof FormSchema>;
|
||||
@@ -26,7 +22,6 @@ const outputList = buildOutputList(initialCompilationValues.outputs);
|
||||
|
||||
const CompilationForm = ({ node }: INextOperatorForm) => {
|
||||
const defaultValues = useFormValues(initialCompilationValues, node);
|
||||
const ownerTenantId = useOwnerTenantId();
|
||||
|
||||
const form = useForm<CompilationFormSchemaType>({
|
||||
defaultValues,
|
||||
@@ -38,9 +33,6 @@ const CompilationForm = ({ node }: INextOperatorForm) => {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<LargeModelFormField
|
||||
ownerTenantId={ownerTenantId}
|
||||
></LargeModelFormField>
|
||||
<CompilationTemplateFormField name="compilation_template_group_ids"></CompilationTemplateFormField>
|
||||
<Output list={outputList}></Output>
|
||||
</FormWrapper>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;
|
||||
|
||||
const CanvasCategoryIconMap: Record<string, LucideIcon> = {
|
||||
[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 (
|
||||
<Button variant={'ghost'} size={'sm'}>
|
||||
<Icon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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 && (
|
||||
<Button variant={'ghost'} size={'sm'}>
|
||||
<Route />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
icon={<AgentTypeIcon data={data} />}
|
||||
extra={<AgentTags tags={data.tags} />}
|
||||
showReleaseTime
|
||||
/>
|
||||
|
||||
@@ -116,6 +116,7 @@ export default function AgentTemplates() {
|
||||
loading={loading}
|
||||
visible={creatingVisible}
|
||||
hideModal={hideCreatingModal}
|
||||
canvasCategory={template?.canvas_category as AgentCategory}
|
||||
onOk={handleOk}
|
||||
></CreateAgentDialog>
|
||||
)}
|
||||
|
||||
80
web/src/pages/agents/compilation-template-card.tsx
Normal file
80
web/src/pages/agents/compilation-template-card.tsx
Normal file
@@ -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<string, LucideIcon> = {
|
||||
[CompilationTemplateScope.File]: FileText,
|
||||
[CompilationTemplateScope.Dataset]: Database,
|
||||
};
|
||||
|
||||
function ScopeIcon({ scope }: { scope?: string }) {
|
||||
const Icon = scope ? ScopeIconMap[scope] : null;
|
||||
|
||||
return Icon ? <Icon className="size-4 text-text-secondary shrink-0" /> : 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 (
|
||||
<Card className="group cursor-pointer h-full" onClick={onClick}>
|
||||
<CardContent className="p-4 flex gap-3">
|
||||
<RAGFlowAvatar
|
||||
avatar={data.avatar}
|
||||
name={data.name}
|
||||
className="w-8 h-8 shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
||||
<section className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<h3 className="text-base font-normal truncate text-text-primary">
|
||||
{data.name}
|
||||
</h3>
|
||||
<ScopeIcon scope={data.scope} />
|
||||
</div>
|
||||
|
||||
<CompilationTemplateDropdown data={data} onDelete={onDelete}>
|
||||
<MoreButton />
|
||||
</CompilationTemplateDropdown>
|
||||
</section>
|
||||
|
||||
<p className="text-sm text-text-secondary line-clamp-1">
|
||||
{data.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{kinds.map((kind) => (
|
||||
<Badge key={kind} variant="secondary">
|
||||
{formatKindLabel(t, kind)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
58
web/src/pages/agents/compilation-template-dropdown.tsx
Normal file
58
web/src/pages/agents/compilation-template-dropdown.tsx
Normal file
@@ -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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div onClick={(e) => e.stopPropagation()}>{children}</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<ConfirmDeleteDialog
|
||||
title={t('setting.deleteTemplateGroupModalTitle')}
|
||||
content={{
|
||||
title: t('setting.deleteTemplateGroupModalContent'),
|
||||
node: <ConfirmDeleteDialogNode name={data.name} />,
|
||||
}}
|
||||
onOk={handleDelete}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="text-state-error"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{t('common.delete')}
|
||||
<Trash2 className="size-4" />
|
||||
</DropdownMenuItem>
|
||||
</ConfirmDeleteDialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum FlowType {
|
||||
Agent = 'agent',
|
||||
Compiler = 'compiler',
|
||||
Flow = 'flow',
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent data-testid="agent-create-modal">
|
||||
<DialogContent data-testid="agent-create-modal" className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('flow.createGraph')}</DialogTitle>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{DialogIcon && <DialogIcon className="size-5" />}
|
||||
{dialogTitle}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<CreateAgentForm
|
||||
hideModal={hideModal}
|
||||
onOk={onOk}
|
||||
shouldChooseAgent={shouldChooseAgent}
|
||||
loading={loading}
|
||||
showTypeCards={!canvasCategory}
|
||||
></CreateAgentForm>
|
||||
<DialogFooter>
|
||||
<ButtonLoading
|
||||
data-testid="agent-save"
|
||||
type="submit"
|
||||
form={TagRenameId}
|
||||
loading={loading}
|
||||
>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -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<any> & {
|
||||
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 (
|
||||
<section className="flex gap-10">
|
||||
{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 (
|
||||
<Card
|
||||
key={val}
|
||||
@@ -55,16 +75,8 @@ function FlowTypeCards({ value, onChange }: FlowTypeCardProps) {
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
{val === FlowType.Agent ? (
|
||||
<BrainCircuit className="size-6" />
|
||||
) : (
|
||||
<Route className="size-6" />
|
||||
)}
|
||||
<p>
|
||||
{t(
|
||||
`flow.${val === FlowType.Agent ? 'createAgent' : 'createPipeline'}`,
|
||||
)}
|
||||
</p>
|
||||
<Icon className="size-6" />
|
||||
<p>{t(`flow.${config.labelKey}`)}</p>
|
||||
</div>
|
||||
{isActive && <Check />}
|
||||
</CardContent>
|
||||
@@ -87,15 +99,26 @@ export type FormSchemaType = z.infer<typeof FormSchema>;
|
||||
export function CreateAgentForm({
|
||||
hideModal,
|
||||
onOk,
|
||||
shouldChooseAgent = false,
|
||||
loading,
|
||||
showTypeCards = false,
|
||||
}: CreateAgentFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<FormSchemaType>({
|
||||
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 && (
|
||||
<RAGFlowFormItem
|
||||
required
|
||||
name="type"
|
||||
@@ -119,8 +142,27 @@ export function CreateAgentForm({
|
||||
<FlowTypeCards></FlowTypeCards>
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
<NameFormField></NameFormField>
|
||||
{!isCompiler && <NameFormField></NameFormField>}
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={hideModal}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
{isCompiler ? (
|
||||
<Button type="button" data-testid="agent-next" onClick={handleNext}>
|
||||
{t('common.next')}
|
||||
</Button>
|
||||
) : (
|
||||
<ButtonLoading
|
||||
data-testid="agent-save"
|
||||
type="submit"
|
||||
form={TagRenameId}
|
||||
loading={loading}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</ButtonLoading>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 ? (
|
||||
<article
|
||||
className="size-full min-w-0 flex flex-col"
|
||||
data-testid="agents-list"
|
||||
>
|
||||
<header className="mb-4 min-w-0 px-5 pt-8">
|
||||
<ListFilterBar
|
||||
title={t('flow.agents')}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
icon="agents"
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
>
|
||||
<article
|
||||
className="size-full min-w-0 flex flex-col"
|
||||
data-testid="agents-list"
|
||||
>
|
||||
<header className="mb-4 min-w-0 px-5 pt-8">
|
||||
<ListFilterBar
|
||||
title={t('flow.agents')}
|
||||
icon="agents"
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
>
|
||||
{isCompilation ? (
|
||||
<Button
|
||||
onClick={handleAddCompilation}
|
||||
data-testid="create-compilation-template"
|
||||
>
|
||||
<Plus className="size-[1em]" />
|
||||
</Button>
|
||||
) : (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger data-testid="create-agent" asChild>
|
||||
<Button>
|
||||
<Plus className="size-[1em]" />
|
||||
{t('flow.createGraph')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent data-testid="agent-create-menu">
|
||||
@@ -133,93 +176,99 @@ export default function Agents() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ListFilterBar>
|
||||
</header>
|
||||
)}
|
||||
</ListFilterBar>
|
||||
</header>
|
||||
|
||||
{data.length ? (
|
||||
<>
|
||||
<CardContainer className="flex-1 overflow-auto px-5">
|
||||
{data.map((x) => {
|
||||
return (
|
||||
{data.length ? (
|
||||
<>
|
||||
<CardContainer className="flex-1 overflow-auto px-5">
|
||||
{isCompilation
|
||||
? (data as any[]).map((item) => (
|
||||
<CompilationTemplateCard
|
||||
key={item.id}
|
||||
data={item}
|
||||
onClick={handleEditCompilation(item.id)}
|
||||
onDelete={handleDeleteCompilation}
|
||||
/>
|
||||
))
|
||||
: data.map((x) => (
|
||||
<AgentCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showAgentRenameModal={showAgentRenameModal}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
))}
|
||||
</CardContainer>
|
||||
|
||||
<footer className="mt-4 px-5 pb-5">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</footer>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14"
|
||||
isSearch
|
||||
type={EmptyCardType.Agent}
|
||||
onClick={() => showCreatingModal()}
|
||||
<footer className="mt-4 px-5 pb-5">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
) : listLoading ? (
|
||||
<article className="size-full" data-testid="agents-list"></article>
|
||||
) : (
|
||||
<article
|
||||
className="size-full flex items-center justify-center"
|
||||
data-testid="agents-list"
|
||||
>
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14 !cursor-default"
|
||||
type={EmptyCardType.Agent}
|
||||
tabIndex={-1}
|
||||
// onClick={() => showCreatingModal()}
|
||||
>
|
||||
<ul className="flex flex-col gap-y-5 text-text-secondary text-sm pt-5">
|
||||
<li data-testid="agents-empty-create">
|
||||
<Button
|
||||
variant="static"
|
||||
size="auto"
|
||||
onClick={showCreatingModal}
|
||||
>
|
||||
<Clipboard className="size-[1em]" />
|
||||
{t('flow.createFromBlank')}
|
||||
</Button>
|
||||
</li>
|
||||
</footer>
|
||||
</>
|
||||
) : searchString ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14"
|
||||
isSearch
|
||||
type={EmptyCardType.Agent}
|
||||
onClick={() => showCreatingModal()}
|
||||
/>
|
||||
</div>
|
||||
) : listLoading ? null : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14 !cursor-default"
|
||||
type={EmptyCardType.Agent}
|
||||
tabIndex={-1}
|
||||
// onClick={() => showCreatingModal()}
|
||||
>
|
||||
<ul className="flex flex-col gap-y-5 text-text-secondary text-sm pt-5">
|
||||
<li data-testid="agents-empty-create">
|
||||
<Button
|
||||
variant="static"
|
||||
size="auto"
|
||||
onClick={showCreatingModal}
|
||||
>
|
||||
<Clipboard className="size-[1em]" />
|
||||
{t('flow.createFromBlank')}
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Button
|
||||
asLink
|
||||
variant="static"
|
||||
size="auto"
|
||||
to={Routes.AgentTemplates}
|
||||
>
|
||||
<ClipboardPlus className="size-[1em]" />
|
||||
{t('flow.createFromTemplate')}
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
asLink
|
||||
variant="static"
|
||||
size="auto"
|
||||
to={Routes.AgentTemplates}
|
||||
>
|
||||
<ClipboardPlus className="size-[1em]" />
|
||||
{t('flow.createFromTemplate')}
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Button variant="static" size="auto" onClick={handleImportJson}>
|
||||
<FileInput className="size-[1em]" />
|
||||
{t('flow.importJsonFile')}
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</EmptyAppCard>
|
||||
</article>
|
||||
)}
|
||||
<li>
|
||||
<Button
|
||||
variant="static"
|
||||
size="auto"
|
||||
onClick={handleImportJson}
|
||||
>
|
||||
<FileInput className="size-[1em]" />
|
||||
{t('flow.importJsonFile')}
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</EmptyAppCard>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
|
||||
{agentRenameVisible && (
|
||||
<RenameDialog
|
||||
@@ -234,7 +283,6 @@ export default function Agents() {
|
||||
loading={loading}
|
||||
visible={creatingVisible}
|
||||
hideModal={hideCreatingModal}
|
||||
shouldChooseAgent
|
||||
onOk={handleCreateAgentOrPipeline}
|
||||
></CreateAgentDialog>
|
||||
)}
|
||||
|
||||
@@ -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<SelectWithSearchFlagOptionType[]>(() => {
|
||||
return templates.map((template) => ({
|
||||
value: template.template_id,
|
||||
@@ -24,13 +26,13 @@ export function RepresentationSelect({
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="truncate">{template.template_name}</span>
|
||||
<span className="text-xs text-text-secondary shrink-0">
|
||||
{formatKindLabel(template.kind)}
|
||||
{formatKindLabel(t, template.kind)}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
keywords: [template.template_name, template.kind],
|
||||
}));
|
||||
}, [templates]);
|
||||
}, [templates, t]);
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { type SelectWithSearchFlagOptionType } from '@/components/originui/select-with-search';
|
||||
import { getEntityDisplayName } from '@/components/structure-graph/adapters';
|
||||
import { type ClickableNode } from '@/components/structure-graph/representation-renderer';
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
import { useFetchDocumentStructureGraph } from '@/hooks/use-document-request';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelectedTemplate } from './use-selected-template';
|
||||
|
||||
export function useGraphEntitySearch(
|
||||
onNodeClick?: (node: ClickableNode) => 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<SelectWithSearchFlagOptionType[]>(
|
||||
() =>
|
||||
(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,
|
||||
};
|
||||
}
|
||||
@@ -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 (
|
||||
<section className="p-5 rounded-2xl h-full flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<RepresentationSelect
|
||||
templates={templates}
|
||||
value={selectedTemplateId}
|
||||
onChange={setSelectedTemplateId}
|
||||
onChange={handleTemplateChange}
|
||||
/>
|
||||
<div className="relative flex items-center gap-2">
|
||||
<ExpandableSearchInput
|
||||
value={searchKeyword}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t('chunk.search', 'Search')}
|
||||
/>
|
||||
{isGraphKind ? (
|
||||
<SelectWithSearch
|
||||
options={entityOptions}
|
||||
value={graphSelectValue}
|
||||
onChange={handleSelectEntity}
|
||||
placeholder={t('knowledgeDetails.searchEntity')}
|
||||
allowClear
|
||||
onNoMatchEnter={handleNoMatchEnter}
|
||||
disableAutoSelectOnEnter
|
||||
/>
|
||||
) : (
|
||||
<ExpandableSearchInput
|
||||
value={searchKeyword}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t('chunk.search', 'Search')}
|
||||
/>
|
||||
)}
|
||||
{templates.length > 0 && (
|
||||
<ConfirmDeleteDialog onOk={handleDelete}>
|
||||
<Button
|
||||
@@ -76,12 +91,8 @@ function Representation({ onNodeClick }: RepresentationProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{loading && (
|
||||
<div className="mt-6 text-text-secondary">
|
||||
{t('common.loading', 'Loading...')}
|
||||
</div>
|
||||
)}
|
||||
{!loading && templates.length === 0 && (
|
||||
{loading && !data && <SkeletonCard className="mt-6" />}
|
||||
{!(loading && !data) && templates.length === 0 && (
|
||||
<div className="mt-6 text-text-secondary">
|
||||
{t(
|
||||
'chunk.representationEmpty',
|
||||
@@ -89,11 +100,12 @@ function Representation({ onNodeClick }: RepresentationProps) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!loading && templates.length > 0 && (
|
||||
{!(loading && !data) && templates.length > 0 && (
|
||||
<RepresentationRenderer
|
||||
template={selectedTemplate}
|
||||
searchKeyword={searchKeyword}
|
||||
onNodeClick={handleNodeClick}
|
||||
highlightNodeId={highlightNodeId}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -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<ViewMode, ViewMode.Tree>;
|
||||
|
||||
export const ViewModeGenerateTypeMap: Record<GenerableViewMode, GenerateType> =
|
||||
{
|
||||
[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,
|
||||
};
|
||||
|
||||
127
web/src/pages/dataset/compilation/dataset-structure-view.tsx
Normal file
127
web/src/pages/dataset/compilation/dataset-structure-view.tsx
Normal file
@@ -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<SelectWithSearchFlagOptionType[]>(
|
||||
() =>
|
||||
(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 <CompilationLoadingCard />;
|
||||
}
|
||||
|
||||
if (!template && !graphKeywords) {
|
||||
return (
|
||||
<CompilationEmptyState
|
||||
type={kind}
|
||||
disabled={!canGenerate}
|
||||
data={structureRunData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex-1 min-h-0 overflow-hidden flex border-border-button rounded-xl flex-col">
|
||||
{kind === ViewMode.Graph && (
|
||||
<div className="flex justify-end px-4 pt-4">
|
||||
<SelectWithSearch
|
||||
options={entityOptions}
|
||||
value={selectedEntityName || graphKeywords}
|
||||
onChange={handleSelectEntity}
|
||||
placeholder={t('knowledgeDetails.searchEntity')}
|
||||
allowClear
|
||||
triggerClassName="w-96 max-w-full"
|
||||
onNoMatchEnter={handleNoMatchEnter}
|
||||
disableAutoSelectOnEnter
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<RepresentationRenderer
|
||||
template={template}
|
||||
highlightNodeId={selectedEntityName || null}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<EmptyStateType, GenerateType> = {
|
||||
'llm-wiki': GenerateType.Artifact,
|
||||
skills: GenerateType.ToSkills,
|
||||
};
|
||||
|
||||
const TitleKeyMap: Record<EmptyStateType, string> = {
|
||||
'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<EmptyStateType, string> = {
|
||||
'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);
|
||||
|
||||
|
||||
@@ -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>(ViewMode.LlmWiki);
|
||||
|
||||
const handleSwitchToLlmWiki = useCallback(() => {
|
||||
setViewMode(ViewMode.LlmWiki);
|
||||
const viewOptions = useMemo<SelectWithSearchFlagOptionType[]>(() => {
|
||||
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 (
|
||||
<section className="flex flex-col p-4 gap-4 h-full">
|
||||
@@ -51,37 +86,19 @@ export default function Compilation() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={viewMode === ViewMode.LlmWiki ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleSwitchToLlmWiki}
|
||||
>
|
||||
{t('knowledgeDetails.llmWiki')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={viewMode === ViewMode.Skills ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleSwitchToSkills}
|
||||
>
|
||||
To Skills
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={viewMode === ViewMode.Tree ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleSwitchToTree}
|
||||
>
|
||||
{t('knowledgeDetails.navTree')}
|
||||
</Button>
|
||||
</div>
|
||||
<SelectWithSearch
|
||||
options={viewOptions}
|
||||
value={viewMode}
|
||||
onChange={handleViewModeChange}
|
||||
triggerClassName="w-96"
|
||||
/>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
{viewMode === ViewMode.LlmWiki && <LlmWikiView />}
|
||||
{viewMode === ViewMode.Skills && <SkillsView />}
|
||||
{viewMode === ViewMode.Tree && <NavTreeView />}
|
||||
{structureKind && <DatasetStructureView kind={structureKind} />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<CompilationEmptyState
|
||||
type="llm-wiki"
|
||||
type={ViewMode.LlmWiki}
|
||||
disabled={!canGenerate}
|
||||
data={artifactRunData}
|
||||
/>
|
||||
@@ -88,6 +90,9 @@ export function LlmWikiView() {
|
||||
onSelectArtifact={handleSelectArtifact}
|
||||
onClearArtifact={clearSelectedArtifact}
|
||||
onClearWiki={clearSelectedArtifact}
|
||||
updateSheetOpen={updateSheetOpen}
|
||||
onUpdateSheetOpenChange={setUpdateSheetOpen}
|
||||
traceData={artifactRunData}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
@@ -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 (
|
||||
<CompilationEmptyState
|
||||
type="skills"
|
||||
type={ViewMode.Skills}
|
||||
disabled={!canGenerate}
|
||||
data={skillRunData}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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<SelectWithSearchFlagOptionType[]>(
|
||||
() =>
|
||||
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 (
|
||||
<aside className="size-full flex flex-col p-5">
|
||||
<section className="flex items-center justify-between pb-5">
|
||||
<Tabs value={tab} onValueChange={onTabChange}>
|
||||
<TabsList className="grid grid-cols-2 w-80">
|
||||
<TabsTrigger value={LeftPanelTab.Contents}>
|
||||
{t('knowledgeDetails.contents')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={LeftPanelTab.Graph}>
|
||||
{t('knowledgeDetails.graph')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex items-center justify-between pb-5">
|
||||
{hasChanges && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
onClick={handleUpdateClick}
|
||||
disabled={updateLoading}
|
||||
>
|
||||
{t('knowledgeDetails.update', { defaultValue: 'Update' })}
|
||||
{newlyUploaded > 0 && (
|
||||
<Badge variant="success" className="ml-1">
|
||||
{newlyUploaded}
|
||||
</Badge>
|
||||
)}
|
||||
{removed > 0 && (
|
||||
<Badge variant="destructive" className="ml-1">
|
||||
{removed}
|
||||
</Badge>
|
||||
)}
|
||||
<WandSparkles />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('knowledgeDetails.updateTooltip', {
|
||||
newlyUploaded,
|
||||
removed,
|
||||
defaultValue:
|
||||
'{{newlyUploaded}} new, {{removed}} removed documents found. Click to compile and merge into current Wiki.',
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<ConfirmDeleteDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
@@ -107,7 +116,17 @@ export function WikiLeftPanel({
|
||||
<Trash2 className="size-[1em]" />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</section>
|
||||
</div>
|
||||
<Tabs value={tab} onValueChange={onTabChange} className="pb-5">
|
||||
<TabsList className="grid grid-cols-2 w-80">
|
||||
<TabsTrigger value={LeftPanelTab.Contents}>
|
||||
{t('knowledgeDetails.contents')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={LeftPanelTab.Graph}>
|
||||
{t('knowledgeDetails.graph')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
{tab === LeftPanelTab.Contents && (
|
||||
@@ -117,25 +136,19 @@ export function WikiLeftPanel({
|
||||
/>
|
||||
)}
|
||||
{tab === LeftPanelTab.Graph && (
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<SelectWithSearch
|
||||
options={entityOptions}
|
||||
value={selectedEntitySlug}
|
||||
onChange={handleSelectEntity}
|
||||
placeholder={t('knowledgeDetails.searchEntity')}
|
||||
allowClear
|
||||
triggerClassName="w-96 max-w-full"
|
||||
/>
|
||||
<ArtifactForceGraph
|
||||
data={data}
|
||||
show
|
||||
mapNodeToValue={mapNodeToValue}
|
||||
onNodeClick={onSelectArtifact}
|
||||
highlightNodeId={selectedArtifact?.slug}
|
||||
/>
|
||||
</div>
|
||||
<WikiGraphPanel
|
||||
selectedArtifact={selectedArtifact}
|
||||
onSelectArtifact={onSelectArtifact}
|
||||
onClearArtifact={onClearArtifact}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<WikiUpdateSheet
|
||||
open={updateSheetOpen}
|
||||
onOpenChange={onUpdateSheetOpenChange}
|
||||
data={traceData}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<IFetchArtifactGraphRequestParams | undefined>(
|
||||
() =>
|
||||
graphKeywords
|
||||
? { keywords: graphKeywords, top_n: GraphSearchTopN }
|
||||
: undefined,
|
||||
[graphKeywords],
|
||||
);
|
||||
const { data } = useFetchArtifactGraph(graphParams);
|
||||
|
||||
const entityOptions = useMemo<SelectWithSearchFlagOptionType[]>(
|
||||
() =>
|
||||
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 (
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<SelectWithSearch
|
||||
options={entityOptions}
|
||||
value={selectedEntitySlug || graphKeywords}
|
||||
onChange={handleSelectEntity}
|
||||
placeholder={t('knowledgeDetails.searchEntity')}
|
||||
allowClear
|
||||
triggerClassName="w-96 max-w-full"
|
||||
onNoMatchEnter={handleNoMatchEnter}
|
||||
disableAutoSelectOnEnter
|
||||
/>
|
||||
<ArtifactForceGraph
|
||||
data={data}
|
||||
show
|
||||
mapNodeToValue={mapNodeToValue}
|
||||
onNodeClick={onSelectArtifact}
|
||||
highlightNodeId={selectedArtifact?.slug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export function WikiNavBar({
|
||||
} = useCreateDirectory();
|
||||
|
||||
return (
|
||||
<div className="size-full flex flex-col gap-3 px-3">
|
||||
<div className="size-full flex flex-col gap-3">
|
||||
<SearchInput
|
||||
placeholder={t('common.search')}
|
||||
value={searchString}
|
||||
@@ -91,6 +91,7 @@ export function WikiNavBar({
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
onClick={handleShowCreateDialog}
|
||||
className="hidden"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
|
||||
@@ -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 (
|
||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
||||
<SheetContent className="flex flex-col">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{t('knowledgeDetails.updateSheetTitle', {
|
||||
defaultValue: 'Update Wiki',
|
||||
})}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 min-h-0 flex flex-col gap-4 pt-4">
|
||||
<div className="flex items-center gap-2 text-text-primary">
|
||||
<Logs className="size-5" />
|
||||
<span>
|
||||
{t('knowledgeDetails.artifact', { defaultValue: 'Artifact' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn('bg-border-button h-1 rounded-full', {
|
||||
'w-[calc(100%-100px)]': status === 'running',
|
||||
'w-[calc(100%-50px)]': status === 'failed',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn('h-1 rounded-full', {
|
||||
'bg-state-error': status === 'failed',
|
||||
'bg-accent-primary': status === 'running',
|
||||
})}
|
||||
style={{ width: `${toFixed(percent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{status === 'running' && (
|
||||
<span>{(toFixed(percent) as string) + '%'}</span>
|
||||
)}
|
||||
{status !== 'failed' && (
|
||||
<span
|
||||
className="text-state-error cursor-pointer"
|
||||
onClick={handlePause}
|
||||
>
|
||||
<CirclePause />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 whitespace-pre-line text-wrap rounded-lg overflow-y-auto scrollbar-auto p-2 bg-bg-base text-sm text-text-secondary">
|
||||
{replaceText(data?.progress_msg || '')}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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, TraceType> = {
|
||||
[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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -91,7 +91,7 @@ function TemplateSidebarItem({
|
||||
</span>
|
||||
{template?.kind && (
|
||||
<span className="ml-2 shrink-0 text-text-secondary">
|
||||
{formatKindLabel(template.kind)}
|
||||
{formatKindLabel(t, template.kind)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, string>;
|
||||
onAdd: (field: Record<string, string>) => 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<string, string>) => {
|
||||
onAdd(field);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[onAdd, onOpenChange],
|
||||
);
|
||||
|
||||
const getFieldLabel = useCallback(
|
||||
(key: string) => {
|
||||
return FieldLabelKeyMap[key] ? t(FieldLabelKeyMap[key]) : startCase(key);
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={`${initialField ? t('setting.editFieldModalTitle') : t('setting.addFieldModalTitle')} - ${startCase(sectionName)}`}
|
||||
size="default"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={handleClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSubmit(handleConfirm)}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form {...form}>
|
||||
<div className="space-y-4">
|
||||
{hasTypeField && (
|
||||
<RAGFlowFormItem name="type" label={getFieldLabel('type')}>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
options={typeOptions}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
handleTypeChange(value);
|
||||
}}
|
||||
placeholder={t('setting.selectFieldType')}
|
||||
allowCustomValue
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{nonTypeKeys.map((key) => (
|
||||
<RAGFlowFormItem key={key} name={key} label={getFieldLabel(key)}>
|
||||
<Textarea
|
||||
placeholder={t('setting.descriptionPlaceholder')}
|
||||
rows={key === 'description' ? 4 : 10}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import MarkdownEditor from '@/components/markdown-editor';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useFetchWikiPresets } from '@/hooks/use-compilation-template-request';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useBlueprintSelection } from '../hooks/use-blueprint-selection';
|
||||
import { FormSchemaType } from '../schema';
|
||||
|
||||
type BlueprintSectionProps = {
|
||||
form: UseFormReturn<FormSchemaType>;
|
||||
selectedTemplateIndex: number;
|
||||
};
|
||||
|
||||
export function BlueprintSection({
|
||||
form,
|
||||
selectedTemplateIndex,
|
||||
}: BlueprintSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: presets } = useFetchWikiPresets();
|
||||
const {
|
||||
selectedValue,
|
||||
options,
|
||||
handleSelect,
|
||||
instructionPath,
|
||||
pageExample,
|
||||
handlePageExampleChange,
|
||||
} = useBlueprintSelection({ form, selectedTemplateIndex, presets });
|
||||
|
||||
if (presets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4 pt-4">
|
||||
<Collapse
|
||||
defaultOpen
|
||||
title={
|
||||
<h3 className="text-base font-medium">{t('setting.blueprints')}</h3>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<SelectWithSearch
|
||||
value={selectedValue}
|
||||
onChange={handleSelect}
|
||||
options={options}
|
||||
placeholder={t('common.selectPlaceholder')}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<RAGFlowFormItem
|
||||
name={instructionPath}
|
||||
label={t('setting.instruction')}
|
||||
>
|
||||
<Textarea rows={6} />
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<div className="flex h-[50vh] min-h-0 flex-col">
|
||||
<MarkdownEditor
|
||||
content={String(pageExample ?? '')}
|
||||
onChange={handlePageExampleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
|
||||
type FieldCardProps = {
|
||||
title?: string;
|
||||
field: Record<string, string>;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export function FieldCard({ title, field, onEdit, onDelete }: FieldCardProps) {
|
||||
return (
|
||||
<Card className="border-border-button bg-transparent group">
|
||||
<CardContent className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="space-y-2 space-x-2 flex-1 min-w-0">
|
||||
{title && (
|
||||
<span className="text-sm font-medium text-text-primary">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={onEdit}
|
||||
className="text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={onDelete}
|
||||
className="text-text-secondary hover:text-state-error"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{field.description && (
|
||||
<p className="text-sm text-text-secondary line-clamp-3">
|
||||
{field.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import JsonEditor from '@/components/json-edit';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Braces } from 'lucide-react';
|
||||
|
||||
interface JsonPreviewSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
value: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function JsonPreviewSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
value,
|
||||
}: JsonPreviewSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8">
|
||||
<Braces className="size-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('setting.jsonPreview')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<SheetContent
|
||||
className="w-1/2 max-w-[700px] flex flex-col"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('setting.jsonPreview')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 min-h-0 mt-4">
|
||||
<JsonEditor
|
||||
value={value}
|
||||
height="100%"
|
||||
options={{ mode: 'tree', modes: ['tree', 'code'] }}
|
||||
defaultExpanded
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FieldCard } from './field-card';
|
||||
|
||||
type SectionFieldGridProps = {
|
||||
fieldsPath: string;
|
||||
sectionName: string;
|
||||
onOpenAddField: () => void;
|
||||
onEditField: (index: number) => void;
|
||||
};
|
||||
|
||||
export function SectionFieldGrid({
|
||||
fieldsPath,
|
||||
sectionName,
|
||||
onOpenAddField,
|
||||
onEditField,
|
||||
}: SectionFieldGridProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const { fields, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: fieldsPath,
|
||||
});
|
||||
|
||||
const isTypedSection = sectionName === 'entity' || sectionName === 'relation';
|
||||
|
||||
const currentFields = useWatch({
|
||||
control: form.control,
|
||||
name: fieldsPath,
|
||||
}) as Record<string, string>[] | undefined;
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{fields.map((field, index) => {
|
||||
const fieldValue = currentFields?.[index] ?? {};
|
||||
return (
|
||||
<FieldCard
|
||||
key={field.id}
|
||||
title={isTypedSection ? fieldValue.type : undefined}
|
||||
field={fieldValue}
|
||||
onEdit={() => onEditField(index)}
|
||||
onDelete={() => remove(index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onOpenAddField}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onOpenAddField();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'border-border-button bg-transparent border-dashed flex flex-col items-center justify-center gap-2 min-h-[140px] cursor-pointer',
|
||||
'hover:border-border-accent hover:text-text-primary text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center gap-2 p-4">
|
||||
<Plus className="size-6" />
|
||||
<span className="text-sm font-medium">{t('setting.addField')}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { ModelTreeSelectFormField } from '@/components/model-tree-select';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { ICompilationTemplateBuiltin } from '@/interfaces/database/compilation-template';
|
||||
import { startCase } from 'lodash';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
import { TreeTemplateFields } from './tree-template-fields';
|
||||
import { useTemplateKindChange } from '../hooks/use-template-kind-change';
|
||||
import { FormSchemaType } from '../schema';
|
||||
import { SectionTitleKeyMap } from '../utils';
|
||||
|
||||
import { useActiveSectionTab } from '../hooks/use-active-section-tab';
|
||||
import { useAvailableKindOptions } from '../hooks/use-available-kind-options';
|
||||
import { useBuiltinTemplate } from '../hooks/use-builtin-template';
|
||||
import { useFieldArrayHandlers } from '../hooks/use-field-array-handlers';
|
||||
import { useFieldModal } from '../hooks/use-field-modal';
|
||||
import { useTemplatePreviewSheets } from '../hooks/use-template-preview-sheets';
|
||||
import { useTemplateSectionData } from '../hooks/use-template-section-data';
|
||||
|
||||
import { AddFieldModal } from './add-field-modal';
|
||||
import { SectionFieldGrid } from './section-field-grid';
|
||||
import { TemplatePreviewHeader } from './template-preview-header';
|
||||
|
||||
type TemplateConfigurationProps = {
|
||||
form: UseFormReturn<FormSchemaType>;
|
||||
builtins: ICompilationTemplateBuiltin[];
|
||||
kindOptions: { label: string; value: string }[];
|
||||
selectedTemplateIndex: number;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function TemplateConfiguration({
|
||||
form,
|
||||
builtins,
|
||||
kindOptions,
|
||||
selectedTemplateIndex,
|
||||
children,
|
||||
}: TemplateConfigurationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
addFieldModalOpen,
|
||||
editingFieldIndex,
|
||||
setEditingFieldIndex,
|
||||
handleModalOpenChange,
|
||||
handleOpenAddField,
|
||||
handleOpenEditField,
|
||||
} = useFieldModal();
|
||||
|
||||
const kind = useWatch({
|
||||
control: form.control,
|
||||
name: `templates.${selectedTemplateIndex}.kind`,
|
||||
});
|
||||
|
||||
const availableKindOptions = useAvailableKindOptions(
|
||||
form,
|
||||
kindOptions,
|
||||
selectedTemplateIndex,
|
||||
);
|
||||
|
||||
const { builtinTemplate, sectionNames } = useBuiltinTemplate(builtins, kind);
|
||||
|
||||
const {
|
||||
jsonSheetOpen,
|
||||
setJsonSheetOpen,
|
||||
workflowSheetOpen,
|
||||
setWorkflowSheetOpen,
|
||||
allFormValues,
|
||||
templateName,
|
||||
} = useTemplatePreviewSheets(form, selectedTemplateIndex);
|
||||
|
||||
const { activeSectionTab, setActiveSectionTab } =
|
||||
useActiveSectionTab(sectionNames);
|
||||
|
||||
const handleKindChange = useTemplateKindChange({
|
||||
form,
|
||||
index: selectedTemplateIndex,
|
||||
builtins,
|
||||
});
|
||||
|
||||
const { activeFieldsPath, builtinSection, editingField } =
|
||||
useTemplateSectionData(
|
||||
form,
|
||||
selectedTemplateIndex,
|
||||
activeSectionTab,
|
||||
builtinTemplate,
|
||||
editingFieldIndex,
|
||||
);
|
||||
|
||||
const { handleAddField } = useFieldArrayHandlers(
|
||||
form,
|
||||
activeFieldsPath,
|
||||
editingFieldIndex,
|
||||
setEditingFieldIndex,
|
||||
);
|
||||
|
||||
const renderSectionTabs = useCallback(
|
||||
(sectionName: string) => {
|
||||
return (
|
||||
sectionName === activeSectionTab && (
|
||||
<SectionFieldGrid
|
||||
key={activeFieldsPath}
|
||||
fieldsPath={activeFieldsPath}
|
||||
sectionName={sectionName}
|
||||
onOpenAddField={handleOpenAddField}
|
||||
onEditField={handleOpenEditField}
|
||||
/>
|
||||
)
|
||||
);
|
||||
},
|
||||
[
|
||||
activeFieldsPath,
|
||||
activeSectionTab,
|
||||
handleOpenAddField,
|
||||
handleOpenEditField,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplatePreviewHeader
|
||||
templateName={templateName}
|
||||
jsonSheetOpen={jsonSheetOpen}
|
||||
onJsonSheetOpenChange={setJsonSheetOpen}
|
||||
workflowSheetOpen={workflowSheetOpen}
|
||||
onWorkflowSheetOpenChange={setWorkflowSheetOpen}
|
||||
allFormValues={allFormValues}
|
||||
/>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto p-5">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<RAGFlowFormItem
|
||||
name={`templates.${selectedTemplateIndex}.name`}
|
||||
label={t('setting.templateName')}
|
||||
required
|
||||
>
|
||||
<Input placeholder={t('common.namePlaceholder')} />
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<RAGFlowFormItem
|
||||
name={`templates.${selectedTemplateIndex}.description`}
|
||||
label={t('setting.templateDescription')}
|
||||
>
|
||||
<Textarea
|
||||
placeholder={t('common.descriptionPlaceholder')}
|
||||
rows={2}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<ModelTreeSelectFormField
|
||||
name={`templates.${selectedTemplateIndex}.llm_id`}
|
||||
label={t('setting.llmForExtraction')}
|
||||
required
|
||||
/>
|
||||
|
||||
<RAGFlowFormItem
|
||||
name={`templates.${selectedTemplateIndex}.kind`}
|
||||
label={t('knowledgeCompilation.builtinTemplates')}
|
||||
required
|
||||
>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
value={field.value}
|
||||
onChange={(value) => handleKindChange(field, value)}
|
||||
disabled={field.disabled}
|
||||
options={availableKindOptions}
|
||||
placeholder={t('common.selectPlaceholder')}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<RAGFlowFormItem
|
||||
name={`templates.${selectedTemplateIndex}.config.global_rules`}
|
||||
label={t('setting.globalRules')}
|
||||
>
|
||||
<Textarea
|
||||
placeholder={t('setting.globalRulesPlaceholder')}
|
||||
rows={8}
|
||||
resize="vertical"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
{kind === CompilationTemplateKind.Tree ? (
|
||||
<TreeTemplateFields index={selectedTemplateIndex} />
|
||||
) : (
|
||||
sectionNames.length > 0 &&
|
||||
activeSectionTab && (
|
||||
<Tabs
|
||||
value={activeSectionTab}
|
||||
onValueChange={setActiveSectionTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full justify-start">
|
||||
{sectionNames.map((sectionName) => (
|
||||
<TabsTrigger
|
||||
key={sectionName}
|
||||
value={sectionName}
|
||||
className="flex-1"
|
||||
>
|
||||
{t(
|
||||
SectionTitleKeyMap[sectionName] ??
|
||||
startCase(sectionName),
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{sectionNames.map((sectionName) => (
|
||||
<TabsContent
|
||||
key={sectionName}
|
||||
value={sectionName}
|
||||
className="mt-4"
|
||||
>
|
||||
{renderSectionTabs(sectionName)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
)
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddFieldModal
|
||||
open={addFieldModalOpen}
|
||||
onOpenChange={handleModalOpenChange}
|
||||
sectionName={activeSectionTab}
|
||||
builtinSection={builtinSection}
|
||||
initialField={editingField}
|
||||
onAdd={handleAddField}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { JsonPreviewSheet } from './json-preview-sheet';
|
||||
import { WorkflowPreviewSheet } from './workflow-preview-sheet';
|
||||
|
||||
interface TemplatePreviewHeaderProps {
|
||||
templateName: string | undefined;
|
||||
jsonSheetOpen: boolean;
|
||||
onJsonSheetOpenChange: (open: boolean) => void;
|
||||
workflowSheetOpen: boolean;
|
||||
onWorkflowSheetOpenChange: (open: boolean) => void;
|
||||
allFormValues: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function TemplatePreviewHeader({
|
||||
templateName,
|
||||
jsonSheetOpen,
|
||||
onJsonSheetOpenChange,
|
||||
workflowSheetOpen,
|
||||
onWorkflowSheetOpenChange,
|
||||
allFormValues,
|
||||
}: TemplatePreviewHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="shrink-0 flex justify-between items-center px-5 py-4 border-b border-border-button">
|
||||
<span className="text-lg font-medium text-text-primary">
|
||||
{templateName || t('setting.templateName')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<JsonPreviewSheet
|
||||
open={jsonSheetOpen}
|
||||
onOpenChange={onJsonSheetOpenChange}
|
||||
value={allFormValues}
|
||||
/>
|
||||
<WorkflowPreviewSheet
|
||||
open={workflowSheetOpen}
|
||||
onOpenChange={onWorkflowSheetOpenChange}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||
import { SwitchFormField } from '@/components/switch-fom-field';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TreeTemplateFieldsProps = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
export function TreeTemplateFields({ index }: TreeTemplateFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Collapse defaultOpen title={t('setting.raptorTreeSettings')}>
|
||||
<div className="space-y-4">
|
||||
<RAGFlowFormItem
|
||||
name={`templates.${index}.config.raptor.prompt`}
|
||||
label={t('setting.summarizationPrompt')}
|
||||
>
|
||||
<Textarea
|
||||
placeholder={t('setting.descriptionPlaceholder')}
|
||||
rows={6}
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<SliderInputFormField
|
||||
name={`templates.${index}.config.raptor.max_token`}
|
||||
label={t('setting.maxToken')}
|
||||
max={2048}
|
||||
min={0}
|
||||
step={1}
|
||||
/>
|
||||
<SliderInputFormField
|
||||
name={`templates.${index}.config.raptor.threshold`}
|
||||
label={t('setting.threshold')}
|
||||
step={0.01}
|
||||
max={1}
|
||||
min={0}
|
||||
/>
|
||||
|
||||
<SwitchFormField
|
||||
name={`templates.${index}.config.raptor.rechunk`}
|
||||
label={t('setting.rechunkByTreeLeaves')}
|
||||
tooltip={t('setting.rechunkByTreeLeavesTip')}
|
||||
vertical={false}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Workflow } from 'lucide-react';
|
||||
|
||||
interface WorkflowPreviewSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function WorkflowPreviewSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: WorkflowPreviewSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8">
|
||||
<Workflow className="size-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('setting.processFlow')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<SheetContent
|
||||
className="w-1/2 max-w-[700px] flex flex-col"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('setting.processFlow')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 min-h-0 mt-4 flex items-center justify-center">
|
||||
<span className="text-text-disabled">
|
||||
{t('setting.processFlowComingSoon')}
|
||||
</span>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useActiveSectionTab = (sectionNames: string[]) => {
|
||||
const [activeSectionTab, setActiveSectionTab] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSectionTab((prev) =>
|
||||
sectionNames.includes(prev) ? prev : (sectionNames[0] ?? ''),
|
||||
);
|
||||
}, [sectionNames]);
|
||||
|
||||
return { activeSectionTab, setActiveSectionTab };
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ICompilationTemplateSection } from '@/interfaces/database/compilation-template';
|
||||
import {
|
||||
createEmptyField,
|
||||
getFieldKeyOrder,
|
||||
getTypeOptionsFromBuiltinSection,
|
||||
} from '../utils';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
type UseAddFieldFormOptions = {
|
||||
open: boolean;
|
||||
builtinSection?: ICompilationTemplateSection;
|
||||
initialField?: Record<string, string>;
|
||||
};
|
||||
|
||||
export const useAddFieldForm = ({
|
||||
open,
|
||||
builtinSection,
|
||||
initialField,
|
||||
}: UseAddFieldFormOptions) => {
|
||||
const form = useForm<Record<string, string>>({
|
||||
defaultValues: {},
|
||||
});
|
||||
|
||||
const fieldKeys = useMemo(() => {
|
||||
const firstField = builtinSection?.fields?.[0];
|
||||
const keys = firstField
|
||||
? Object.keys(firstField)
|
||||
: ['type', 'description', 'rule'];
|
||||
return getFieldKeyOrder(keys);
|
||||
}, [builtinSection]);
|
||||
|
||||
const hasTypeField = fieldKeys.includes('type');
|
||||
|
||||
const typeOptions = useMemo(
|
||||
() => getTypeOptionsFromBuiltinSection(builtinSection),
|
||||
[builtinSection],
|
||||
);
|
||||
|
||||
const buildField = useCallback(
|
||||
(typeValue: string) => {
|
||||
const matched = builtinSection?.fields?.find(
|
||||
(field) => field.type === typeValue,
|
||||
);
|
||||
if (matched) {
|
||||
const normalized: Record<string, string> = {};
|
||||
fieldKeys.forEach((key) => {
|
||||
normalized[key] = (matched as Record<string, string>)[key] ?? '';
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
const empty = createEmptyField(fieldKeys);
|
||||
if (hasTypeField) {
|
||||
empty.type = typeValue;
|
||||
}
|
||||
return empty;
|
||||
},
|
||||
[builtinSection, fieldKeys, hasTypeField],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (initialField) {
|
||||
const normalized: Record<string, string> = {};
|
||||
fieldKeys.forEach((key) => {
|
||||
normalized[key] = initialField[key] ?? '';
|
||||
});
|
||||
form.reset(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstType = typeOptions[0]?.value ?? '';
|
||||
form.reset(buildField(firstType));
|
||||
}, [buildField, fieldKeys, form, initialField, open, typeOptions]);
|
||||
|
||||
const handleTypeChange = useCallback(
|
||||
(value: string) => {
|
||||
if (initialField) {
|
||||
form.setValue('type', value, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nextField = buildField(value);
|
||||
Object.entries(nextField).forEach(([key, val]) => {
|
||||
form.setValue(key, val, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
[buildField, form, initialField],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(onAdd: (field: Record<string, string>) => void) => {
|
||||
return form.handleSubmit((values) => {
|
||||
onAdd(values);
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return {
|
||||
form,
|
||||
fieldKeys,
|
||||
hasTypeField,
|
||||
typeOptions,
|
||||
handleTypeChange,
|
||||
handleSubmit,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
import { FormSchemaType } from '../schema';
|
||||
import { useMemo } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
|
||||
export const useAvailableKindOptions = (
|
||||
form: UseFormReturn<FormSchemaType>,
|
||||
kindOptions: { label: string; value: string }[],
|
||||
selectedTemplateIndex: number,
|
||||
) => {
|
||||
const kind = useWatch({
|
||||
control: form.control,
|
||||
name: `templates.${selectedTemplateIndex}.kind`,
|
||||
});
|
||||
|
||||
const templates = useWatch({ control: form.control, name: 'templates' });
|
||||
|
||||
const availableKindOptions = useMemo(() => {
|
||||
const otherSelectedKinds = new Set(
|
||||
templates
|
||||
?.filter((_, index) => index !== selectedTemplateIndex)
|
||||
.map((template) => template.kind)
|
||||
.filter((value): value is string => Boolean(value)) ?? [],
|
||||
);
|
||||
|
||||
const hasOtherNonArtifactsKind = Array.from(otherSelectedKinds).some(
|
||||
(value) => value !== CompilationTemplateKind.Artifacts,
|
||||
);
|
||||
const hasOtherArtifactsKind = otherSelectedKinds.has(
|
||||
CompilationTemplateKind.Artifacts,
|
||||
);
|
||||
|
||||
return kindOptions.filter((option) => {
|
||||
if (option.value === kind) return true;
|
||||
if (otherSelectedKinds.has(option.value)) return false;
|
||||
if (
|
||||
hasOtherNonArtifactsKind &&
|
||||
option.value === CompilationTemplateKind.Artifacts
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
hasOtherArtifactsKind &&
|
||||
option.value !== CompilationTemplateKind.Artifacts
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [kindOptions, kind, selectedTemplateIndex, templates]);
|
||||
|
||||
return availableKindOptions;
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import { SelectWithSearchFlagOptionType } from '@/components/originui/select-with-search';
|
||||
import { IWikiPreset } from '@/interfaces/database/compilation-template';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FormSchemaType } from '../schema';
|
||||
|
||||
export const CustomBlueprintValue = '__custom__';
|
||||
|
||||
type UseBlueprintSelectionParams = {
|
||||
form: UseFormReturn<FormSchemaType>;
|
||||
selectedTemplateIndex: number;
|
||||
presets: IWikiPreset[];
|
||||
};
|
||||
|
||||
const isSameBlueprintContent = (
|
||||
preset: IWikiPreset,
|
||||
instruction: string,
|
||||
pageExample: string,
|
||||
) =>
|
||||
preset.instruction.trim() === instruction.trim() &&
|
||||
preset.page_example.trim() === pageExample.trim();
|
||||
|
||||
export function useBlueprintSelection({
|
||||
form,
|
||||
selectedTemplateIndex,
|
||||
presets,
|
||||
}: UseBlueprintSelectionParams) {
|
||||
const { t } = useTranslation();
|
||||
const [explicitValue, setExplicitValue] = useState<string>();
|
||||
|
||||
const instructionPath =
|
||||
`templates.${selectedTemplateIndex}.config.instruction` as const;
|
||||
const pageExamplePath =
|
||||
`templates.${selectedTemplateIndex}.config.page_example` as const;
|
||||
const useBlueprintPath =
|
||||
`templates.${selectedTemplateIndex}.config.use_blueprint` as const;
|
||||
|
||||
const instruction = useWatch({
|
||||
control: form.control,
|
||||
name: instructionPath,
|
||||
});
|
||||
const pageExample = useWatch({
|
||||
control: form.control,
|
||||
name: pageExamplePath,
|
||||
});
|
||||
|
||||
const matchedPresetId = useMemo(() => {
|
||||
const currentInstruction = String(instruction ?? '');
|
||||
const currentPageExample = String(pageExample ?? '');
|
||||
if (!currentInstruction.trim() && !currentPageExample.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
return presets.find((preset) =>
|
||||
isSameBlueprintContent(preset, currentInstruction, currentPageExample),
|
||||
)?.id;
|
||||
}, [instruction, pageExample, presets]);
|
||||
|
||||
const selectedValue =
|
||||
explicitValue ?? matchedPresetId ?? CustomBlueprintValue;
|
||||
|
||||
const options = useMemo<SelectWithSearchFlagOptionType[]>(
|
||||
() => [
|
||||
...presets.map((preset) => ({
|
||||
label: preset.id,
|
||||
value: preset.id,
|
||||
})),
|
||||
{ label: t('setting.custom'), value: CustomBlueprintValue },
|
||||
],
|
||||
[presets, t],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (value === selectedValue) return;
|
||||
setExplicitValue(value);
|
||||
|
||||
if (value === CustomBlueprintValue) {
|
||||
const defaultConfig =
|
||||
form.formState.defaultValues?.templates?.[selectedTemplateIndex]
|
||||
?.config;
|
||||
form.setValue(
|
||||
instructionPath,
|
||||
String(defaultConfig?.instruction ?? ''),
|
||||
{ shouldValidate: false },
|
||||
);
|
||||
form.setValue(
|
||||
pageExamplePath,
|
||||
String(defaultConfig?.page_example ?? ''),
|
||||
{ shouldValidate: false },
|
||||
);
|
||||
} else {
|
||||
const preset = presets.find((item) => item.id === value);
|
||||
if (!preset) return;
|
||||
form.setValue(instructionPath, preset.instruction, {
|
||||
shouldValidate: false,
|
||||
});
|
||||
form.setValue(pageExamplePath, preset.page_example, {
|
||||
shouldValidate: false,
|
||||
});
|
||||
}
|
||||
|
||||
form.setValue(useBlueprintPath, true, { shouldValidate: false });
|
||||
},
|
||||
[
|
||||
form,
|
||||
instructionPath,
|
||||
pageExamplePath,
|
||||
presets,
|
||||
selectedTemplateIndex,
|
||||
selectedValue,
|
||||
useBlueprintPath,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePageExampleChange = useCallback(
|
||||
(value: string) => {
|
||||
form.setValue(pageExamplePath, value, { shouldValidate: false });
|
||||
},
|
||||
[form, pageExamplePath],
|
||||
);
|
||||
|
||||
return {
|
||||
selectedValue,
|
||||
options,
|
||||
handleSelect,
|
||||
instructionPath,
|
||||
pageExample,
|
||||
handlePageExampleChange,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ICompilationTemplateBuiltin,
|
||||
ICompilationTemplateSection,
|
||||
} from '@/interfaces/database/compilation-template';
|
||||
import { isConfigMetaKey, sortSectionNames } from '../utils';
|
||||
|
||||
export const useBuiltinTemplate = (
|
||||
builtins: ICompilationTemplateBuiltin[],
|
||||
kind: string,
|
||||
) => {
|
||||
const builtinTemplate = useMemo(
|
||||
() => builtins.find((template) => template.kind === kind),
|
||||
[builtins, kind],
|
||||
);
|
||||
|
||||
const sectionNames = useMemo(() => {
|
||||
const names = Object.keys(builtinTemplate?.config ?? {}).filter((key) => {
|
||||
if (isConfigMetaKey(key)) return false;
|
||||
const section = builtinTemplate?.config?.[key];
|
||||
return (
|
||||
section &&
|
||||
typeof section === 'object' &&
|
||||
Array.isArray((section as ICompilationTemplateSection).fields)
|
||||
);
|
||||
});
|
||||
return sortSectionNames(names);
|
||||
}, [builtinTemplate]);
|
||||
|
||||
return { builtinTemplate, sectionNames };
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ICompilationTemplateGroup } from '@/interfaces/database/compilation-template';
|
||||
|
||||
import { buildFormSchema, FormSchemaType } from '../schema';
|
||||
import { DefaultValues, transformGroupDetailToForm } from '../utils';
|
||||
|
||||
type UseCompilationTemplateGroupFormOptions = {
|
||||
detail?: ICompilationTemplateGroup;
|
||||
defaultLlmId?: string;
|
||||
isCreate: boolean;
|
||||
};
|
||||
|
||||
export const useCompilationTemplateGroupForm = ({
|
||||
detail,
|
||||
defaultLlmId,
|
||||
isCreate,
|
||||
}: UseCompilationTemplateGroupFormOptions) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<FormSchemaType>({
|
||||
resolver: zodResolver(buildFormSchema(t)),
|
||||
defaultValues: DefaultValues,
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (detail) {
|
||||
form.reset(transformGroupDetailToForm(detail));
|
||||
} else if (
|
||||
isCreate &&
|
||||
defaultLlmId &&
|
||||
!form.getValues('templates.0.llm_id')
|
||||
) {
|
||||
form.setValue('templates.0.llm_id', defaultLlmId);
|
||||
}
|
||||
}, [defaultLlmId, detail, form, isCreate]);
|
||||
|
||||
return { form };
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
ICreateCompilationTemplateGroupRequestBody,
|
||||
IUpdateCompilationTemplateGroupRequestBody,
|
||||
} from '@/interfaces/request/compilation-template';
|
||||
|
||||
import { FormSchemaType } from '../schema';
|
||||
import { transformFormToPayload } from '../utils';
|
||||
|
||||
type UseCompilationTemplateGroupSubmitOptions = {
|
||||
isCreate: boolean;
|
||||
id?: string;
|
||||
createGroup: (
|
||||
params: ICreateCompilationTemplateGroupRequestBody,
|
||||
) => Promise<{ code: number } & Record<string, unknown>>;
|
||||
updateGroup: (
|
||||
id: string,
|
||||
params: IUpdateCompilationTemplateGroupRequestBody,
|
||||
) => Promise<{ code: number } & Record<string, unknown>>;
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
export const useCompilationTemplateGroupSubmit = ({
|
||||
isCreate,
|
||||
id,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
onSuccess,
|
||||
}: UseCompilationTemplateGroupSubmitOptions) => {
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormSchemaType) => {
|
||||
const payload = transformFormToPayload(values);
|
||||
let result;
|
||||
if (isCreate) {
|
||||
result = await createGroup(payload);
|
||||
} else if (id) {
|
||||
result = await updateGroup(id, payload);
|
||||
}
|
||||
if (result?.code === 0) {
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
[createGroup, id, isCreate, onSuccess, updateGroup],
|
||||
);
|
||||
|
||||
return { onSubmit };
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import {
|
||||
useCreateCompilationTemplateGroup,
|
||||
useFetchCompilationTemplateGroup,
|
||||
useUpdateCompilationTemplateGroup,
|
||||
} from '@/hooks/use-compilation-template-group-request';
|
||||
import { useFetchBuiltinCompilationTemplates } from '@/hooks/use-compilation-template-request';
|
||||
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';
|
||||
|
||||
import { useCompilationTemplateGroupForm } from './use-compilation-template-group-form';
|
||||
import { useCompilationTemplateGroupSubmit } from './use-compilation-template-group-submit';
|
||||
|
||||
type UseEditNextCompilationTemplateGroupOptions = {
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const useEditNextCompilationTemplateGroup = ({
|
||||
onSuccess,
|
||||
}: UseEditNextCompilationTemplateGroupOptions = {}) => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { navigateToCompilationTemplates } = useNavigatePage();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isCreate = isCreateCompilationTemplateGroup(id);
|
||||
|
||||
const { data: detail } = useFetchCompilationTemplateGroup();
|
||||
const { data: builtins, kindOptions: builtinKindOptions } =
|
||||
useFetchBuiltinCompilationTemplates();
|
||||
const defaultModelDictionary = useFetchDefaultModelDictionary();
|
||||
|
||||
const { createGroup, loading: createLoading } =
|
||||
useCreateCompilationTemplateGroup();
|
||||
const { updateGroup, loading: updateLoading } =
|
||||
useUpdateCompilationTemplateGroup();
|
||||
|
||||
const kindOptions = useMemo(
|
||||
() =>
|
||||
builtinKindOptions.map((option) => ({
|
||||
...option,
|
||||
label: formatKindLabel(t, option.value),
|
||||
})),
|
||||
[builtinKindOptions, t],
|
||||
);
|
||||
|
||||
const { form } = useCompilationTemplateGroupForm({
|
||||
detail,
|
||||
defaultLlmId: defaultModelDictionary.llm_id,
|
||||
isCreate,
|
||||
});
|
||||
|
||||
const { onSubmit } = useCompilationTemplateGroupSubmit({
|
||||
isCreate,
|
||||
id,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
onSuccess: onSuccess ?? navigateToCompilationTemplates,
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
form,
|
||||
kindOptions,
|
||||
builtins,
|
||||
onSubmit,
|
||||
isCreate,
|
||||
isLoading: isCreate ? createLoading : updateLoading,
|
||||
navigateToCompilationTemplates,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ArrayPath, UseFormReturn } from 'react-hook-form';
|
||||
|
||||
import { FormSchemaType } from '../schema';
|
||||
|
||||
export const useFieldArrayHandlers = (
|
||||
form: UseFormReturn<FormSchemaType>,
|
||||
activeFieldsPath: ArrayPath<FormSchemaType>,
|
||||
editingFieldIndex: number | null,
|
||||
setEditingFieldIndex: (index: number | null) => void,
|
||||
) => {
|
||||
const handleAddField = useCallback(
|
||||
(field: Record<string, string>) => {
|
||||
const currentFields =
|
||||
(form.getValues(activeFieldsPath) as
|
||||
| Record<string, string>[]
|
||||
| undefined) ?? [];
|
||||
if (editingFieldIndex !== null) {
|
||||
const newFields = [...currentFields];
|
||||
newFields[editingFieldIndex] = field;
|
||||
form.setValue(activeFieldsPath, newFields, {
|
||||
shouldValidate: false,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
} else {
|
||||
form.setValue(activeFieldsPath, [...currentFields, field], {
|
||||
shouldValidate: false,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
setEditingFieldIndex(null);
|
||||
},
|
||||
[activeFieldsPath, editingFieldIndex, form, setEditingFieldIndex],
|
||||
);
|
||||
|
||||
return { handleAddField };
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const useFieldModal = () => {
|
||||
const [addFieldModalOpen, setAddFieldModalOpen] = useState(false);
|
||||
const [editingFieldIndex, setEditingFieldIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleModalOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setAddFieldModalOpen(open);
|
||||
if (!open) setEditingFieldIndex(null);
|
||||
},
|
||||
[setAddFieldModalOpen],
|
||||
);
|
||||
|
||||
const handleOpenAddField = useCallback(() => {
|
||||
setEditingFieldIndex(null);
|
||||
setAddFieldModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleOpenEditField = useCallback((index: number) => {
|
||||
setEditingFieldIndex(index);
|
||||
setAddFieldModalOpen(true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addFieldModalOpen,
|
||||
editingFieldIndex,
|
||||
setEditingFieldIndex,
|
||||
handleModalOpenChange,
|
||||
handleOpenAddField,
|
||||
handleOpenEditField,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ICompilationTemplateBuiltin } from '@/interfaces/database/compilation-template';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
import { FormSchemaType } from '../schema';
|
||||
import { buildConfigFromBuiltin } from '../utils';
|
||||
|
||||
type FieldLike = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type UseTemplateKindChangeOptions = {
|
||||
form: UseFormReturn<FormSchemaType>;
|
||||
index: number;
|
||||
builtins: ICompilationTemplateBuiltin[];
|
||||
};
|
||||
|
||||
export const useTemplateKindChange = ({
|
||||
form,
|
||||
index,
|
||||
builtins,
|
||||
}: UseTemplateKindChangeOptions) => {
|
||||
return (field: FieldLike, value: string) => {
|
||||
if (value && value !== field.value) {
|
||||
const builtinTemplate = builtins.find(
|
||||
(template) => template.kind === value,
|
||||
);
|
||||
if (builtinTemplate) {
|
||||
form.setValue(
|
||||
`templates.${index}.config`,
|
||||
buildConfigFromBuiltin(
|
||||
builtinTemplate,
|
||||
value,
|
||||
form.getValues(`templates.${index}.llm_id`),
|
||||
),
|
||||
{ shouldValidate: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
field.onChange(value);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
|
||||
import { FormSchemaType } from '../schema';
|
||||
|
||||
export function useTemplatePreviewSheets(
|
||||
form: UseFormReturn<FormSchemaType>,
|
||||
selectedTemplateIndex: number,
|
||||
) {
|
||||
const [jsonSheetOpen, setJsonSheetOpen] = useState(false);
|
||||
const [workflowSheetOpen, setWorkflowSheetOpen] = useState(false);
|
||||
|
||||
const allFormValues = useWatch({ control: form.control });
|
||||
|
||||
const templateName = useWatch({
|
||||
control: form.control,
|
||||
name: `templates.${selectedTemplateIndex}.name`,
|
||||
});
|
||||
|
||||
return {
|
||||
jsonSheetOpen,
|
||||
setJsonSheetOpen,
|
||||
workflowSheetOpen,
|
||||
setWorkflowSheetOpen,
|
||||
allFormValues,
|
||||
templateName,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ArrayPath, UseFormReturn } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
ICompilationTemplateBuiltin,
|
||||
ICompilationTemplateSection,
|
||||
} from '@/interfaces/database/compilation-template';
|
||||
import { FormSchemaType } from '../schema';
|
||||
|
||||
export const useTemplateSectionData = (
|
||||
form: UseFormReturn<FormSchemaType>,
|
||||
selectedTemplateIndex: number,
|
||||
activeSectionTab: string,
|
||||
builtinTemplate: ICompilationTemplateBuiltin | undefined,
|
||||
editingFieldIndex: number | null,
|
||||
) => {
|
||||
const activeSectionPath = `templates.${selectedTemplateIndex}.config.${activeSectionTab}`;
|
||||
const activeFieldsPath =
|
||||
`${activeSectionPath}.fields` as ArrayPath<FormSchemaType>;
|
||||
|
||||
const builtinSection = useMemo(() => {
|
||||
return builtinTemplate?.config?.[activeSectionTab] as
|
||||
| ICompilationTemplateSection
|
||||
| undefined;
|
||||
}, [activeSectionTab, builtinTemplate?.config]);
|
||||
|
||||
const editingField = useMemo(() => {
|
||||
if (editingFieldIndex === null) return undefined;
|
||||
return ((form.getValues(activeFieldsPath) as
|
||||
| Record<string, string>[]
|
||||
| undefined) ?? [])[editingFieldIndex];
|
||||
}, [activeFieldsPath, editingFieldIndex, form]);
|
||||
|
||||
return {
|
||||
activeSectionPath,
|
||||
activeFieldsPath,
|
||||
builtinSection,
|
||||
editingField,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import BackButton from '@/components/back-button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
import { useFetchCompilationTemplateGroup } from '@/hooks/use-compilation-template-group-request';
|
||||
import { Routes } from '@/routes';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { BlueprintSection } from './components/blueprint-section';
|
||||
import { TemplateConfiguration } from './components/template-configuration';
|
||||
import { useEditNextCompilationTemplateGroup } from './hooks/use-edit-next-compilation-template-group';
|
||||
|
||||
const SelectedTemplateIndex = 0;
|
||||
|
||||
const agentsUrl = Routes.Agents;
|
||||
|
||||
export default function EditNextCompilationTemplate() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const navigateToAgents = useCallback(() => {
|
||||
navigate(agentsUrl);
|
||||
}, [navigate]);
|
||||
|
||||
const { form, kindOptions, builtins, onSubmit, isCreate, isLoading } =
|
||||
useEditNextCompilationTemplateGroup({
|
||||
onSuccess: navigateToAgents,
|
||||
});
|
||||
const { data: group } = useFetchCompilationTemplateGroup();
|
||||
|
||||
const selectedKind = useWatch({
|
||||
control: form.control,
|
||||
name: `templates.${SelectedTemplateIndex}.kind`,
|
||||
});
|
||||
|
||||
const isArtifacts = selectedKind === CompilationTemplateKind.Artifacts;
|
||||
|
||||
const handleSave = useMemo(
|
||||
() => form.handleSubmit(onSubmit),
|
||||
[form, onSubmit],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="h-full flex flex-col bg-bg-base">
|
||||
<header className="shrink-0 px-5 py-4 border-b border-border-button flex gap-3 items-center">
|
||||
<BackButton to={agentsUrl} />
|
||||
<h2 className="font-medium text-text-secondary">
|
||||
{isCreate
|
||||
? t('setting.addTemplateGroup')
|
||||
: group?.name || t('setting.editTemplateGroup')}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<Form {...form}>
|
||||
<form className="flex-1 min-h-0 flex flex-col">
|
||||
<TemplateConfiguration
|
||||
form={form}
|
||||
builtins={builtins}
|
||||
kindOptions={kindOptions}
|
||||
selectedTemplateIndex={SelectedTemplateIndex}
|
||||
>
|
||||
{isArtifacts && (
|
||||
<BlueprintSection
|
||||
form={form}
|
||||
selectedTemplateIndex={SelectedTemplateIndex}
|
||||
/>
|
||||
)}
|
||||
</TemplateConfiguration>
|
||||
|
||||
<footer className="shrink-0 px-5 py-4 border-t border-border-button flex items-center justify-end gap-5">
|
||||
<Button type="button" variant="outline" onClick={navigateToAgents}>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button type="button" loading={isLoading} onClick={handleSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</footer>
|
||||
</form>
|
||||
</Form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const buildSectionSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
description: z.string().optional(),
|
||||
fields: z
|
||||
.array(z.record(z.string().min(1, t('setting.fieldDescriptionRequired'))))
|
||||
.min(1),
|
||||
});
|
||||
|
||||
export const buildRaptorConfigSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
prompt: z.string().optional(),
|
||||
max_token: z.number().min(1, t('setting.maxTokenRequired')),
|
||||
threshold: z.number().min(0).max(1),
|
||||
rechunk: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const buildSynthesisSchema = () =>
|
||||
z
|
||||
.object({
|
||||
compile_kwd: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
example: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const buildTemplateSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(1, t('setting.templateNameRequired')),
|
||||
description: z.string().optional(),
|
||||
llm_id: z.string().min(1, t('setting.llmForExtractionRequired')),
|
||||
kind: z.string().min(1, t('setting.templateKindRequired')),
|
||||
config: z.record(
|
||||
z.union([
|
||||
buildRaptorConfigSchema(t),
|
||||
buildSectionSchema(t),
|
||||
buildSynthesisSchema(),
|
||||
z.string(),
|
||||
z.boolean(),
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
export const buildFormSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
avatar: z.string().optional(),
|
||||
templates: z.array(buildTemplateSchema(t)).min(1),
|
||||
});
|
||||
|
||||
export type TemplateSchemaType = z.infer<
|
||||
ReturnType<typeof buildTemplateSchema>
|
||||
>;
|
||||
export type FormSchemaType = z.infer<ReturnType<typeof buildFormSchema>>;
|
||||
@@ -0,0 +1,325 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
ICompilationTemplate,
|
||||
ICompilationTemplateBuiltin,
|
||||
ICompilationTemplateGroup,
|
||||
ICompilationTemplateRaptorConfig,
|
||||
ICompilationTemplateSection,
|
||||
} from '@/interfaces/database/compilation-template';
|
||||
import { ICompilationTemplateConfigRequest } from '@/interfaces/request/compilation-template';
|
||||
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
|
||||
import { FormSchemaType, TemplateSchemaType } from './schema';
|
||||
|
||||
export const DefaultFieldKeys = ['type', 'description', 'rule'];
|
||||
|
||||
export const splitExampleToBlueprintFields = (
|
||||
example: string,
|
||||
): { instruction: string; page_example: string } => {
|
||||
const trimmed = example.trim();
|
||||
const separatorIndex = trimmed.indexOf('\n\n');
|
||||
if (separatorIndex === -1) {
|
||||
return { instruction: trimmed, page_example: '' };
|
||||
}
|
||||
return {
|
||||
instruction: trimmed.slice(0, separatorIndex).trim(),
|
||||
page_example: trimmed.slice(separatorIndex + 2).trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const FieldKeyOrders = [
|
||||
DefaultFieldKeys,
|
||||
['statement', 'subject'],
|
||||
['definition_excerpt', 'term'],
|
||||
];
|
||||
|
||||
export const getFieldKeyOrder = (keys: string[]): string[] => {
|
||||
const sortedKeys = [...keys].sort();
|
||||
return (
|
||||
FieldKeyOrders.find((order) => isEqual([...order].sort(), sortedKeys)) ??
|
||||
keys
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultTemplateValues: TemplateSchemaType = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
description: '',
|
||||
llm_id: '',
|
||||
kind: '',
|
||||
config: {
|
||||
kind: '',
|
||||
llm_id: '',
|
||||
global_rules: '',
|
||||
example: '',
|
||||
instruction: '',
|
||||
page_example: '',
|
||||
use_blueprint: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultValues: FormSchemaType = {
|
||||
name: '',
|
||||
description: '',
|
||||
avatar: '',
|
||||
templates: [DefaultTemplateValues],
|
||||
};
|
||||
|
||||
export const isConfigMetaKey = (key: string) =>
|
||||
[
|
||||
'kind',
|
||||
'llm_id',
|
||||
'global_rules',
|
||||
'example',
|
||||
'instruction',
|
||||
'page_example',
|
||||
'synthesis',
|
||||
'use_blueprint',
|
||||
].includes(key);
|
||||
|
||||
export const createEmptyField = (keys: string[]) =>
|
||||
Object.fromEntries(keys.map((key) => [key, '']));
|
||||
|
||||
export const normalizeSection = (
|
||||
section?: ICompilationTemplateSection,
|
||||
): ICompilationTemplateSection => {
|
||||
const fields = section?.fields ?? [];
|
||||
return {
|
||||
description: section?.description ?? '',
|
||||
fields:
|
||||
fields.length > 0
|
||||
? fields.map((field) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(field).map(([key, value]) => [key, value ?? '']),
|
||||
),
|
||||
)
|
||||
: [createEmptyField(DefaultFieldKeys)],
|
||||
};
|
||||
};
|
||||
|
||||
export const buildConfigFromBuiltin = (
|
||||
builtinTemplate: ICompilationTemplateBuiltin,
|
||||
kind: string,
|
||||
llmId: string,
|
||||
): TemplateSchemaType['config'] => {
|
||||
const example =
|
||||
typeof builtinTemplate.config?.example === 'string'
|
||||
? builtinTemplate.config.example
|
||||
: '';
|
||||
const sections: TemplateSchemaType['config'] = {
|
||||
kind,
|
||||
llm_id: llmId,
|
||||
global_rules:
|
||||
typeof builtinTemplate.config?.global_rules === 'string'
|
||||
? builtinTemplate.config.global_rules
|
||||
: '',
|
||||
example:
|
||||
typeof builtinTemplate.config?.example === 'string'
|
||||
? builtinTemplate.config.example
|
||||
: '',
|
||||
...(typeof builtinTemplate.config?.synthesis === 'object' &&
|
||||
builtinTemplate.config?.synthesis !== null
|
||||
? {
|
||||
synthesis: builtinTemplate.config
|
||||
.synthesis as TemplateSchemaType['config']['synthesis'],
|
||||
}
|
||||
: {}),
|
||||
use_blueprint:
|
||||
kind === CompilationTemplateKind.Artifacts && example.length > 0,
|
||||
};
|
||||
|
||||
if (kind === CompilationTemplateKind.Artifacts && example.length > 0) {
|
||||
const { instruction, page_example } =
|
||||
splitExampleToBlueprintFields(example);
|
||||
sections.instruction = instruction;
|
||||
sections.page_example = page_example;
|
||||
}
|
||||
|
||||
if (kind === CompilationTemplateKind.Tree) {
|
||||
const builtinRaptor: ICompilationTemplateRaptorConfig =
|
||||
builtinTemplate.config?.raptor ?? {};
|
||||
return {
|
||||
...sections,
|
||||
raptor: {
|
||||
prompt: builtinRaptor.prompt ?? '',
|
||||
max_token: builtinRaptor.max_token ?? 512,
|
||||
threshold: builtinRaptor.threshold ?? 0.1,
|
||||
rechunk: builtinRaptor.rechunk ?? false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Object.entries(builtinTemplate.config ?? {}).forEach(([key, value]) => {
|
||||
if (isConfigMetaKey(key)) return;
|
||||
sections[key] = normalizeSection(
|
||||
value as ICompilationTemplateSection,
|
||||
) as TemplateSchemaType['config'][string];
|
||||
});
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
export const transformDetailToForm = (
|
||||
detail: ICompilationTemplate,
|
||||
): TemplateSchemaType => {
|
||||
const config = detail.config ?? {};
|
||||
const example = typeof config.example === 'string' ? config.example : '';
|
||||
const base: TemplateSchemaType['config'] = {
|
||||
kind: config.kind ?? '',
|
||||
llm_id: config.llm_id ?? '',
|
||||
global_rules: config.global_rules ?? '',
|
||||
example: typeof config.example === 'string' ? config.example : '',
|
||||
...(typeof config.synthesis === 'object' && config.synthesis !== null
|
||||
? {
|
||||
synthesis:
|
||||
config.synthesis as TemplateSchemaType['config']['synthesis'],
|
||||
}
|
||||
: {}),
|
||||
use_blueprint:
|
||||
detail.kind === CompilationTemplateKind.Artifacts && example.length > 0,
|
||||
};
|
||||
|
||||
if (detail.kind === CompilationTemplateKind.Artifacts && example.length > 0) {
|
||||
const { instruction, page_example } =
|
||||
splitExampleToBlueprintFields(example);
|
||||
base.instruction = instruction;
|
||||
base.page_example = page_example;
|
||||
}
|
||||
|
||||
if (detail.kind === CompilationTemplateKind.Tree) {
|
||||
const raptor: ICompilationTemplateRaptorConfig = config.raptor ?? {};
|
||||
return {
|
||||
id: detail.id,
|
||||
name: detail.name ?? '',
|
||||
description: detail.description ?? '',
|
||||
llm_id: config.llm_id ?? '',
|
||||
kind: detail.kind ?? '',
|
||||
config: {
|
||||
...base,
|
||||
raptor: {
|
||||
prompt: raptor.prompt ?? '',
|
||||
max_token: raptor.max_token ?? 512,
|
||||
threshold: raptor.threshold ?? 0.1,
|
||||
rechunk: raptor.rechunk ?? false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
if (isConfigMetaKey(key)) return;
|
||||
base[key] = normalizeSection(
|
||||
value as ICompilationTemplateSection,
|
||||
) as TemplateSchemaType['config'][string];
|
||||
});
|
||||
|
||||
return {
|
||||
id: detail.id,
|
||||
name: detail.name ?? '',
|
||||
description: detail.description ?? '',
|
||||
llm_id: config.llm_id ?? '',
|
||||
kind: detail.kind ?? '',
|
||||
config: base,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformGroupDetailToForm = (
|
||||
detail: ICompilationTemplateGroup,
|
||||
): FormSchemaType => {
|
||||
const templates = (detail.templates ?? []).map((template) =>
|
||||
transformDetailToForm(template),
|
||||
);
|
||||
|
||||
return {
|
||||
name: detail.name ?? '',
|
||||
description: detail.description ?? '',
|
||||
avatar: detail.avatar ?? '',
|
||||
templates: templates.length > 0 ? templates : [DefaultTemplateValues],
|
||||
};
|
||||
};
|
||||
|
||||
export const transformTemplateToPayload = (template: TemplateSchemaType) => {
|
||||
const config: ICompilationTemplateConfigRequest = {
|
||||
kind: template.kind,
|
||||
llm_id: template.llm_id,
|
||||
};
|
||||
|
||||
Object.entries(template.config).forEach(([key, value]) => {
|
||||
if (key === 'kind' || key === 'llm_id') return;
|
||||
if (key === 'instruction' || key === 'page_example') return;
|
||||
if (key === 'synthesis') {
|
||||
config[key] = value as ICompilationTemplateConfigRequest[string];
|
||||
return;
|
||||
}
|
||||
if (isConfigMetaKey(key)) {
|
||||
if (typeof value === 'string') config[key] = value;
|
||||
} else {
|
||||
config[key] = value as ICompilationTemplateConfigRequest[string];
|
||||
}
|
||||
});
|
||||
|
||||
if (template.kind === CompilationTemplateKind.Artifacts) {
|
||||
if (template.config.use_blueprint) {
|
||||
const instruction = String(template.config.instruction ?? '').trim();
|
||||
const pageExample = String(template.config.page_example ?? '').trim();
|
||||
config.example = [instruction, pageExample].filter(Boolean).join('\n\n');
|
||||
} else {
|
||||
config.example = '';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
kind: template.kind,
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformFormToPayload = (values: FormSchemaType) => {
|
||||
const template = values.templates[0];
|
||||
return {
|
||||
name: template?.name ?? values.name ?? '',
|
||||
description: template?.description ?? values.description,
|
||||
avatar: values.avatar || undefined,
|
||||
templates: values.templates.map((template) =>
|
||||
transformTemplateToPayload(template),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const SectionTitleKeyMap: Record<string, string> = {
|
||||
entity: 'setting.entitySpecification',
|
||||
relation: 'setting.relationSpecification',
|
||||
concept: 'setting.conceptSpecification',
|
||||
claim: 'setting.claimSpecification',
|
||||
};
|
||||
|
||||
export const SectionPriority = ['entity', 'relation'];
|
||||
|
||||
export const sortSectionNames = (names: string[]): string[] => {
|
||||
const priority = SectionPriority.filter((name) => names.includes(name));
|
||||
const rest = names.filter((name) => !SectionPriority.includes(name));
|
||||
return [...priority, ...rest];
|
||||
};
|
||||
|
||||
export const FieldLabelKeyMap: Record<string, string> = {
|
||||
type: 'setting.fieldType',
|
||||
description: 'setting.fieldDescription',
|
||||
rule: 'setting.fieldRule',
|
||||
};
|
||||
|
||||
export const getTypeOptionsFromBuiltinSection = (
|
||||
builtinSection?: ICompilationTemplateSection,
|
||||
) => {
|
||||
const typeSet = new Set<string>();
|
||||
builtinSection?.fields?.forEach((field) => {
|
||||
if (field.type) typeSet.add(field.type);
|
||||
});
|
||||
return Array.from(typeSet)
|
||||
.sort()
|
||||
.map((value) => ({ label: value, value }));
|
||||
};
|
||||
@@ -27,7 +27,10 @@ export default function CompilationTemplates() {
|
||||
} = useFetchCompilationTemplateGroupsByPage();
|
||||
|
||||
const { deleteGroup } = useDeleteCompilationTemplateGroup();
|
||||
const { navigateToCompilationTemplate } = useNavigatePage();
|
||||
const {
|
||||
navigateToCompilationTemplate,
|
||||
navigateToCompilationTemplateEditNext,
|
||||
} = useNavigatePage();
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(page: number, pageSize?: number) => {
|
||||
@@ -40,6 +43,10 @@ export default function CompilationTemplates() {
|
||||
navigateToCompilationTemplate('create')();
|
||||
}, [navigateToCompilationTemplate]);
|
||||
|
||||
const handleAddNext = useCallback(() => {
|
||||
navigateToCompilationTemplateEditNext()();
|
||||
}, [navigateToCompilationTemplateEditNext]);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
await deleteGroup(id);
|
||||
@@ -73,6 +80,10 @@ export default function CompilationTemplates() {
|
||||
<Plus />
|
||||
{t('setting.addTemplateGroup')}
|
||||
</Button>
|
||||
<Button onClick={handleAddNext}>
|
||||
<Plus />
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
@@ -85,7 +96,7 @@ export default function CompilationTemplates() {
|
||||
<TemplateCard
|
||||
key={item.id}
|
||||
data={item}
|
||||
onClick={navigateToCompilationTemplate(item.id)}
|
||||
onClick={navigateToCompilationTemplateEditNext(item.id)}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { TemplateDropdown } from './template-dropdown';
|
||||
@@ -28,6 +29,7 @@ function ScopeIcon({ scope }: { scope?: string }) {
|
||||
}
|
||||
|
||||
export function TemplateCard({ data, onClick, onDelete }: TemplateCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const kinds = useMemo(
|
||||
() => Array.from(new Set((data.templates ?? []).map((item) => item.kind))),
|
||||
[data.templates],
|
||||
@@ -63,7 +65,7 @@ export function TemplateCard({ data, onClick, onDelete }: TemplateCardProps) {
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{kinds.map((kind) => (
|
||||
<Badge key={kind} variant="secondary">
|
||||
{formatKindLabel(kind)}
|
||||
{formatKindLabel(t, kind)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { cn } from '@/lib/utils';
|
||||
import { Routes } from '@/routes';
|
||||
import { TFunction } from 'i18next';
|
||||
import {
|
||||
Columns3Cog,
|
||||
LucideBox,
|
||||
LucideLogOut,
|
||||
LucideMessagesSquare,
|
||||
@@ -47,11 +46,6 @@ const menuItems = (t: TFunction) => [
|
||||
label: 'MCP',
|
||||
key: Routes.Mcp,
|
||||
},
|
||||
{
|
||||
icon: <Columns3Cog className="size-[1em]" />,
|
||||
label: t('setting.compilationTemplates'),
|
||||
key: Routes.CompilationTemplates,
|
||||
},
|
||||
{
|
||||
icon: <LucideUsers className="size-[1em]" />,
|
||||
label: t('setting.team'),
|
||||
|
||||
@@ -45,6 +45,7 @@ export enum Routes {
|
||||
Prompt = '/prompt',
|
||||
CompilationTemplates = '/compilation-templates',
|
||||
CompilationTemplatesCreateNext = '/compilation-templates/create-next',
|
||||
CompilationTemplatesEditNext = '/compilation-templates/edit-next',
|
||||
DataSource = '/data-source',
|
||||
DataSourceDetailPage = '/data-source-detail-page',
|
||||
ChatChannel = '/chat-channel',
|
||||
@@ -334,6 +335,18 @@ const routeConfigOptions = [
|
||||
Component: () =>
|
||||
import('@/pages/user-setting/compilation-templates/create-next'),
|
||||
},
|
||||
{
|
||||
path: Routes.CompilationTemplatesEditNext,
|
||||
layout: false,
|
||||
Component: () =>
|
||||
import('@/pages/user-setting/compilation-templates/edit-next'),
|
||||
},
|
||||
{
|
||||
path: `${Routes.CompilationTemplatesEditNext}/:id`,
|
||||
layout: false,
|
||||
Component: () =>
|
||||
import('@/pages/user-setting/compilation-templates/edit-next'),
|
||||
},
|
||||
{
|
||||
path: `${Routes.SearchShare}`,
|
||||
Component: () => import('@/pages/next-search/share'),
|
||||
|
||||
@@ -4,7 +4,11 @@ import request from '@/utils/next-request';
|
||||
export const getDocumentStructureGraph = (
|
||||
datasetId: string,
|
||||
documentId: string,
|
||||
) => request.get(api.documentStructureGraph(datasetId, documentId));
|
||||
keywords?: string,
|
||||
) =>
|
||||
request.get(api.documentStructureGraph(datasetId, documentId), {
|
||||
params: keywords ? { keywords } : undefined,
|
||||
});
|
||||
|
||||
export const deleteDocumentStructureGraph = (
|
||||
datasetId: string,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IRenameTag } from '@/interfaces/database/dataset';
|
||||
import {
|
||||
IFetchArtifactGraphRequestParams,
|
||||
IFetchArtifactListRequestParams,
|
||||
IFetchArtifactTopicListRequestParams,
|
||||
IFetchDocumentListRequestBody,
|
||||
@@ -415,9 +416,21 @@ export const getArtifactPage = (
|
||||
|
||||
export const getArtifactGraph = (
|
||||
datasetId: string,
|
||||
params?: { node?: string },
|
||||
params?: IFetchArtifactGraphRequestParams,
|
||||
) => request.get(api.getArtifactGraph(datasetId), { params });
|
||||
|
||||
export const getArtifactsAlteration = (datasetId: string) =>
|
||||
request.get(api.artifactsAlteration(datasetId));
|
||||
|
||||
export const getArtifactsStructure = (
|
||||
datasetId: string,
|
||||
kind: string,
|
||||
keywords?: string,
|
||||
) =>
|
||||
request.get(api.artifactsStructure(datasetId), {
|
||||
params: keywords ? { kind, keywords } : { kind },
|
||||
});
|
||||
|
||||
export const updateArtifactPage = (
|
||||
datasetId: string,
|
||||
pageType: string,
|
||||
|
||||
@@ -159,6 +159,8 @@ export default {
|
||||
`${restAPIv1}/datasets/${datasetId}/ingestions/summary`,
|
||||
artifactsList: (datasetId: string) =>
|
||||
`${restAPIv1}/datasets/${datasetId}/artifacts`,
|
||||
artifactsAlteration: (datasetId: string) =>
|
||||
`${restAPIv1}/datasets/${datasetId}/artifacts/alteration`,
|
||||
artifactsTopicList: (datasetId: string) =>
|
||||
`${restAPIv1}/datasets/${datasetId}/artifacts_topics`,
|
||||
getArtifactPage: (datasetId: string, pageType: string, slug: string) =>
|
||||
@@ -169,6 +171,8 @@ export default {
|
||||
`${restAPIv1}/datasets/${datasetId}/commits/${commitId}`,
|
||||
getArtifactGraph: (datasetId: string) =>
|
||||
`${restAPIv1}/datasets/${datasetId}/artifacts/graph`,
|
||||
artifactsStructure: (datasetId: string) =>
|
||||
`${restAPIv1}/datasets/${datasetId}/artifacts_structure`,
|
||||
clearWiki: (datasetId: string) =>
|
||||
`${restAPIv1}/datasets/${datasetId}/artifacts`,
|
||||
getDatasetSkillTree: (datasetId: string) =>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { CompilationTemplateKind } from '@/constants/compilation';
|
||||
import { TFunction } from 'i18next';
|
||||
import { capitalize, lowerCase } from 'lodash';
|
||||
|
||||
export function formatKindLabel(kind: string): string {
|
||||
export function formatKindLabel(t: TFunction, kind: string): string {
|
||||
if (kind === CompilationTemplateKind.Artifacts) {
|
||||
return 'Wiki';
|
||||
}
|
||||
if (kind === CompilationTemplateKind.KnowledgeGraph) {
|
||||
return t('knowledgeDetails.structureGraph');
|
||||
}
|
||||
return capitalize(lowerCase(kind));
|
||||
}
|
||||
export const isCreateCompilationTemplateGroup = (
|
||||
|
||||
Reference in New Issue
Block a user