Compare commits

..

4 Commits

Author SHA1 Message Date
664bc0b961 Feat: Displays the loading status of the data flow log #9869 (#10347)
### What problem does this PR solve?

Feat: Displays the loading status of the data flow log #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-28 19:38:46 +08:00
f4cc4dbd30 Fix: Interoperate with the pipeline rerun and unbindTask interfaces. #9869 (#10346)
### What problem does this PR solve?

Fix: Interoperate with the pipeline rerun and unbindTask interfaces.
#9869

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-28 19:32:19 +08:00
cce361d774 Feat: Filter the agent list by owner and category #9869 (#10344)
### What problem does this PR solve?

Feat: Filter the agent list by owner and category #9869
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-28 18:43:20 +08:00
7a63b6386e Feat: limit pipeline operation logs to 1000 records (#10341)
### What problem does this PR solve?

 Limit pipeline operation logs to 1000 records.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-09-28 18:42:19 +08:00
40 changed files with 814 additions and 371 deletions

View File

@ -15,6 +15,7 @@
# #
import json import json
import logging import logging
import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from peewee import fn from peewee import fn
@ -81,6 +82,14 @@ class PipelineOperationLogService(CommonService):
cls.model.update_date, cls.model.update_date,
] ]
@classmethod
def save(cls, **kwargs):
"""
wrap this function in a transaction
"""
sample_obj = cls.model(**kwargs).save(force_insert=True)
return sample_obj
@classmethod @classmethod
@DB.connection_context() @DB.connection_context()
def create(cls, document_id, pipeline_id, task_type, fake_document_ids=[], dsl: str = "{}"): def create(cls, document_id, pipeline_id, task_type, fake_document_ids=[], dsl: str = "{}"):
@ -163,7 +172,19 @@ class PipelineOperationLogService(CommonService):
log["create_date"] = datetime_format(datetime.now()) log["create_date"] = datetime_format(datetime.now())
log["update_time"] = current_timestamp() log["update_time"] = current_timestamp()
log["update_date"] = datetime_format(datetime.now()) log["update_date"] = datetime_format(datetime.now())
with DB.atomic():
obj = cls.save(**log) obj = cls.save(**log)
limit = int(os.getenv("PIPELINE_OPERATION_LOG_LIMIT", 1))
total = cls.model.select().where(cls.model.kb_id == document.kb_id).count()
if total > limit:
keep_ids = [m.id for m in cls.model.select(cls.model.id).where(cls.model.kb_id == document.kb_id).order_by(cls.model.create_time.desc()).limit(limit)]
deleted = cls.model.delete().where(cls.model.kb_id == document.kb_id, cls.model.id.not_in(keep_ids)).execute()
logging.info(f"[PipelineOperationLogService] Cleaned {deleted} old logs, kept latest {limit} for {document.kb_id}")
return obj return obj
@classmethod @classmethod

View File

@ -1,3 +1,4 @@
import { AgentCategory } from '@/constants/agent';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useFetchAgentList } from '@/hooks/use-agent-request'; import { useFetchAgentList } from '@/hooks/use-agent-request';
import { buildSelectOptions } from '@/utils/component-util'; import { buildSelectOptions } from '@/utils/component-util';
@ -33,8 +34,8 @@ export function DataFlowSelect(props: IProps) {
const toDataPipLine = () => { const toDataPipLine = () => {
toDataPipeline?.(); toDataPipeline?.();
}; };
const { data: dataPipelineOptions, loading } = useFetchAgentList({ const { data: dataPipelineOptions } = useFetchAgentList({
canvas_category: 'dataflow_canvas', canvas_category: AgentCategory.DataflowCanvas,
}); });
const options = useMemo(() => { const options = useMemo(() => {
const option = buildSelectOptions( const option = buildSelectOptions(

View File

@ -53,12 +53,15 @@ type GraphRagItemsProps = {
marginBottom?: boolean; marginBottom?: boolean;
className?: string; className?: string;
data: IGenerateLogButtonProps; data: IGenerateLogButtonProps;
onDelete?: () => void;
}; };
export function UseGraphRagFormField({ export function UseGraphRagFormField({
data, data,
onDelete,
}: { }: {
data: IGenerateLogButtonProps; data: IGenerateLogButtonProps;
onDelete?: () => void;
}) { }) {
const form = useFormContext(); const form = useFormContext();
const { t } = useTranslate('knowledgeConfiguration'); const { t } = useTranslate('knowledgeConfiguration');
@ -84,6 +87,7 @@ export function UseGraphRagFormField({
></Switch> */} ></Switch> */}
<GenerateLogButton <GenerateLogButton
{...data} {...data}
onDelete={onDelete}
className="w-full text-text-secondary" className="w-full text-text-secondary"
status={1} status={1}
type={GenerateType.KnowledgeGraph} type={GenerateType.KnowledgeGraph}
@ -106,6 +110,7 @@ const GraphRagItems = ({
marginBottom = false, marginBottom = false,
className = 'p-10', className = 'p-10',
data, data,
onDelete,
}: GraphRagItemsProps) => { }: GraphRagItemsProps) => {
const { t } = useTranslate('knowledgeConfiguration'); const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext(); const form = useFormContext();
@ -131,7 +136,10 @@ const GraphRagItems = ({
return ( return (
<FormContainer className={cn({ 'mb-4': marginBottom }, className)}> <FormContainer className={cn({ 'mb-4': marginBottom }, className)}>
<UseGraphRagFormField data={data}></UseGraphRagFormField> <UseGraphRagFormField
data={data}
onDelete={onDelete}
></UseGraphRagFormField>
{useRaptor && ( {useRaptor && (
<> <>
<EntityTypesFormField name="parser_config.graphrag.entity_types"></EntityTypesFormField> <EntityTypesFormField name="parser_config.graphrag.entity_types"></EntityTypesFormField>

View File

@ -56,7 +56,13 @@ const Prompt = 'parser_config.raptor.prompt';
// The three types "table", "resume" and "one" do not display this configuration. // The three types "table", "resume" and "one" do not display this configuration.
const RaptorFormFields = ({ data }: { data: IGenerateLogButtonProps }) => { const RaptorFormFields = ({
data,
onDelete,
}: {
data: IGenerateLogButtonProps;
onDelete: () => void;
}) => {
const form = useFormContext(); const form = useFormContext();
const { t } = useTranslate('knowledgeConfiguration'); const { t } = useTranslate('knowledgeConfiguration');
const useRaptor = useWatch({ name: UseRaptorField }); const useRaptor = useWatch({ name: UseRaptorField });
@ -106,6 +112,7 @@ const RaptorFormFields = ({ data }: { data: IGenerateLogButtonProps }) => {
<FormControl> <FormControl>
<GenerateLogButton <GenerateLogButton
{...data} {...data}
onDelete={onDelete}
className="w-full text-text-secondary" className="w-full text-text-secondary"
status={1} status={1}
type={GenerateType.Raptor} type={GenerateType.Raptor}

View File

@ -47,3 +47,8 @@ export const initialLlmBaseValues = {
presence_penalty: 0.4, presence_penalty: 0.4,
max_tokens: 256, max_tokens: 256,
}; };
export enum AgentCategory {
AgentCanvas = 'agent_canvas',
DataflowCanvas = 'dataflow_canvas',
}

View File

@ -1,4 +1,5 @@
import { FileUploadProps } from '@/components/file-upload'; import { FileUploadProps } from '@/components/file-upload';
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
import message from '@/components/ui/message'; import message from '@/components/ui/message';
import { AgentGlobals } from '@/constants/agent'; import { AgentGlobals } from '@/constants/agent';
import { import {
@ -33,6 +34,7 @@ import {
} from './logic-hooks'; } from './logic-hooks';
export const enum AgentApiAction { export const enum AgentApiAction {
FetchAgentListByPage = 'fetchAgentListByPage',
FetchAgentList = 'fetchAgentList', FetchAgentList = 'fetchAgentList',
UpdateAgentSetting = 'updateAgentSetting', UpdateAgentSetting = 'updateAgentSetting',
DeleteAgent = 'deleteAgent', DeleteAgent = 'deleteAgent',
@ -114,16 +116,36 @@ export const useFetchAgentListByPage = () => {
const { searchString, handleInputChange } = useHandleSearchChange(); const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter(); const { pagination, setPagination } = useGetPaginationWithRouter();
const debouncedSearchString = useDebounce(searchString, { wait: 500 }); const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { filterValue, handleFilterSubmit } = useHandleFilterSubmit();
const canvasCategory = Array.isArray(filterValue.canvasCategory)
? filterValue.canvasCategory
: [];
const owner = filterValue.owner;
const requestParams = {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
canvas_category:
canvasCategory.length === 1 ? canvasCategory[0] : undefined,
owner_ids: '',
};
if (Array.isArray(owner) && owner.length > 0) {
requestParams.owner_ids =
`${owner[0]}` + owner.slice(1).map((id) => `&owner_ids=${id}`);
}
const { data, isFetching: loading } = useQuery<{ const { data, isFetching: loading } = useQuery<{
canvas: IFlow[]; canvas: IFlow[];
total: number; total: number;
}>({ }>({
queryKey: [ queryKey: [
AgentApiAction.FetchAgentList, AgentApiAction.FetchAgentListByPage,
{ {
debouncedSearchString, debouncedSearchString,
...pagination, ...pagination,
filterValue,
}, },
], ],
placeholderData: (previousData) => { placeholderData: (previousData) => {
@ -134,13 +156,9 @@ export const useFetchAgentListByPage = () => {
}, },
gcTime: 0, gcTime: 0,
queryFn: async () => { queryFn: async () => {
const { data } = await agentService.listCanvasTeam( const { data } = await agentService.listCanvas(
{ {
params: { params: requestParams,
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
}, },
true, true,
); );
@ -164,6 +182,8 @@ export const useFetchAgentListByPage = () => {
handleInputChange: onInputChange, handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total }, pagination: { ...pagination, total: data?.total },
setPagination, setPagination,
filterValue,
handleFilterSubmit,
}; };
}; };
@ -181,7 +201,7 @@ export const useUpdateAgentSetting = () => {
if (ret?.data?.code === 0) { if (ret?.data?.code === 0) {
message.success('success'); message.success('success');
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentList], queryKey: [AgentApiAction.FetchAgentListByPage],
}); });
} else { } else {
message.error(ret?.data?.data); message.error(ret?.data?.data);
@ -205,7 +225,7 @@ export const useDeleteAgent = () => {
const { data } = await agentService.removeCanvas({ canvasIds }); const { data } = await agentService.removeCanvas({ canvasIds });
if (data.code === 0) { if (data.code === 0) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentList], queryKey: [AgentApiAction.FetchAgentListByPage],
}); });
} }
return data?.data ?? []; return data?.data ?? [];
@ -289,7 +309,7 @@ export const useSetAgent = (showMessage: boolean = true) => {
); );
} }
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentList], queryKey: [AgentApiAction.FetchAgentListByPage],
}); });
} }
return data; return data;
@ -396,13 +416,11 @@ export const useUploadCanvasFileWithProgress = (
return { data, loading, uploadCanvasFile: mutateAsync }; return { data, loading, uploadCanvasFile: mutateAsync };
}; };
export const useFetchMessageTrace = ( export const useFetchMessageTrace = (canvasId?: string) => {
isStopFetchTrace: boolean,
canvasId?: string,
) => {
const { id } = useParams(); const { id } = useParams();
const queryId = id || canvasId; const queryId = id || canvasId;
const [messageId, setMessageId] = useState(''); const [messageId, setMessageId] = useState('');
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
const { const {
data, data,
@ -422,11 +440,19 @@ export const useFetchMessageTrace = (
message_id: messageId, message_id: messageId,
}); });
return data?.data ?? []; return Array.isArray(data?.data) ? data?.data : [];
}, },
}); });
return { data, loading, refetch, setMessageId, messageId }; return {
data,
loading,
refetch,
setMessageId,
messageId,
isStopFetchTrace,
setISStopFetchTrace,
};
}; };
export const useTestDbConnect = () => { export const useTestDbConnect = () => {
@ -657,17 +683,14 @@ export const useFetchPrompt = () => {
}; };
export const useFetchAgentList = ({ export const useFetchAgentList = ({
canvas_category = 'agent_canvas', canvas_category,
}: IPipeLineListRequest): { }: IPipeLineListRequest) => {
data: { const { data, isFetching: loading } = useQuery<{
canvas: IFlow[]; canvas: IFlow[];
total: number; total: number;
}; }>({
loading: boolean; queryKey: [AgentApiAction.FetchAgentList],
} => { initialData: { canvas: [], total: 0 },
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchPipeLineList'],
initialData: [],
gcTime: 0, gcTime: 0,
queryFn: async () => { queryFn: async () => {
const { data } = await fetchPipeLineList({ canvas_category }); const { data } = await fetchPipeLineList({ canvas_category });
@ -699,3 +722,18 @@ export const useCancelDataflow = () => {
return { data, loading, cancelDataflow: mutateAsync }; return { data, loading, cancelDataflow: mutateAsync };
}; };
// export const useFetchKnowledgeList = () => {
// const { data, isFetching: loading } = useQuery<IFlow[]>({
// queryKey: [AgentApiAction.FetchAgentList],
// initialData: [],
// gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
// queryFn: async () => {
// const { data } = await agentService.listCanvas();
// return data?.data ?? [];
// },
// });
// return { list: data, loading };
// };

View File

@ -30,6 +30,7 @@ export const enum KnowledgeApiAction {
FetchKnowledgeDetail = 'fetchKnowledgeDetail', FetchKnowledgeDetail = 'fetchKnowledgeDetail',
FetchKnowledgeGraph = 'fetchKnowledgeGraph', FetchKnowledgeGraph = 'fetchKnowledgeGraph',
FetchMetadata = 'fetchMetadata', FetchMetadata = 'fetchMetadata',
FetchKnowledgeList = 'fetchKnowledgeList',
} }
export const useKnowledgeBaseId = (): string => { export const useKnowledgeBaseId = (): string => {
@ -304,3 +305,25 @@ export function useFetchKnowledgeMetadata(kbIds: string[] = []) {
return { data, loading }; return { data, loading };
} }
export const useFetchKnowledgeList = (
shouldFilterListWithoutDocument: boolean = false,
): {
list: IKnowledge[];
loading: boolean;
} => {
const { data, isFetching: loading } = useQuery({
queryKey: [KnowledgeApiAction.FetchKnowledgeList],
initialData: [],
gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
queryFn: async () => {
const { data } = await listDataset();
const list = data?.data?.kbs ?? [];
return shouldFilterListWithoutDocument
? list.filter((x: IKnowledge) => x.chunk_num > 0)
: list;
},
});
return { list: data, loading };
};

View File

@ -30,6 +30,7 @@ export interface ISwitchForm {
no: string; no: string;
} }
import { AgentCategory } from '@/constants/agent';
import { Edge, Node } from '@xyflow/react'; import { Edge, Node } from '@xyflow/react';
import { IReference, Message } from './chat'; import { IReference, Message } from './chat';
@ -273,5 +274,5 @@ export interface IPipeLineListRequest {
keywords?: string; keywords?: string;
orderby?: string; orderby?: string;
desc?: boolean; desc?: boolean;
canvas_category?: 'agent_canvas' | 'dataflow_canvas'; canvas_category?: AgentCategory;
} }

View File

@ -5,6 +5,7 @@ export interface IDocumentInfo {
create_date: string; create_date: string;
create_time: number; create_time: number;
created_by: string; created_by: string;
nickname: string;
id: string; id: string;
kb_id: string; kb_id: string;
location: string; location: string;

View File

@ -1771,6 +1771,12 @@ Important structured information may include: names, dates, locations, events, k
cancel: 'Cancel', cancel: 'Cancel',
swicthPromptMessage: swicthPromptMessage:
'The prompt word will change. Please confirm whether to abandon the existing prompt word?', 'The prompt word will change. Please confirm whether to abandon the existing prompt word?',
tokenizerFieldsOptions: {
text: 'Text',
keywords: 'Keywords',
questions: 'Questions',
summary: 'Augmented Context',
},
}, },
}, },
}; };

View File

@ -924,8 +924,3 @@ export enum AgentExceptionMethod {
Comment = 'comment', Comment = 'comment',
Goto = 'goto', Goto = 'goto',
} }
export enum AgentCategory {
AgentCanvas = 'agent_canvas',
DataflowCanvas = 'dataflow_canvas',
}

View File

@ -23,7 +23,7 @@ import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { t } from 'i18next'; import { t } from 'i18next';
import { get, isEmpty, isEqual, uniqWith } from 'lodash'; import { get, isEmpty, isEqual, uniqWith } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import JsonView from 'react18-json-view'; import JsonView from 'react18-json-view';
import { Operator } from '../constant'; import { Operator } from '../constant';
import { useCacheChatLog } from '../hooks/use-cache-chat-log'; import { useCacheChatLog } from '../hooks/use-cache-chat-log';
@ -116,12 +116,12 @@ export const WorkFlowTimeline = ({
isShare, isShare,
}: LogFlowTimelineProps) => { }: LogFlowTimelineProps) => {
// const getNode = useGraphStore((state) => state.getNode); // const getNode = useGraphStore((state) => state.getNode);
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
const { data: traceData, setMessageId } = useFetchMessageTrace( const {
isStopFetchTrace, data: traceData,
canvasId, setMessageId,
); setISStopFetchTrace,
} = useFetchMessageTrace(canvasId);
useEffect(() => { useEffect(() => {
setMessageId(currentMessageId); setMessageId(currentMessageId);
@ -133,7 +133,7 @@ export const WorkFlowTimeline = ({
useEffect(() => { useEffect(() => {
setISStopFetchTrace(!sendLoading); setISStopFetchTrace(!sendLoading);
}, [sendLoading]); }, [sendLoading, setISStopFetchTrace]);
const startedNodeList = useMemo(() => { const startedNodeList = useMemo(() => {
const finish = currentEventListWithoutMessage?.some( const finish = currentEventListWithoutMessage?.some(
@ -151,7 +151,7 @@ export const WorkFlowTimeline = ({
} }
return pre; return pre;
}, []); }, []);
}, [currentEventListWithoutMessage, sendLoading]); }, [currentEventListWithoutMessage, sendLoading, setISStopFetchTrace]);
const getElapsedTime = (nodeId: string) => { const getElapsedTime = (nodeId: string) => {
if (nodeId === 'begin') { if (nodeId === 'begin') {

View File

@ -2,10 +2,10 @@ import { HomeCard } from '@/components/home-card';
import { MoreButton } from '@/components/more-button'; import { MoreButton } from '@/components/more-button';
import { SharedBadge } from '@/components/shared-badge'; import { SharedBadge } from '@/components/shared-badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { AgentCategory } from '@/constants/agent';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IFlow } from '@/interfaces/database/agent'; import { IFlow } from '@/interfaces/database/agent';
import { DatabaseZap } from 'lucide-react'; import { Route } from 'lucide-react';
import { AgentCategory } from '../agent/constant';
import { AgentDropdown } from './agent-dropdown'; import { AgentDropdown } from './agent-dropdown';
import { useRenameAgent } from './use-rename-agent'; import { useRenameAgent } from './use-rename-agent';
@ -33,7 +33,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
icon={ icon={
data.canvas_category === AgentCategory.DataflowCanvas && ( data.canvas_category === AgentCategory.DataflowCanvas && (
<Button variant={'ghost'} size={'sm'}> <Button variant={'ghost'} size={'sm'}>
<DatabaseZap /> <Route />
</Button> </Button>
) )
} }

View File

@ -1,7 +1,7 @@
import { AgentCategory } from '@/constants/agent';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request'; import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request';
import { DSL } from '@/interfaces/database/agent'; import { DSL } from '@/interfaces/database/agent';
import { AgentCategory } from '@/pages/agent/constant';
import { import {
BeginId, BeginId,
Operator, Operator,

View File

@ -0,0 +1,23 @@
import { FilterCollection } from '@/components/list-filter-bar/interface';
import { useFetchAgentList } from '@/hooks/use-agent-request';
import { buildOwnersFilter, groupListByType } from '@/utils/list-filter-util';
import { useMemo } from 'react';
export function useSelectFilters() {
const { data } = useFetchAgentList({});
const canvasCategory = useMemo(() => {
return groupListByType(data.canvas, 'canvas_category', 'canvas_category');
}, [data.canvas]);
const filters: FilterCollection[] = [
buildOwnersFilter(data.canvas),
{
field: 'canvasCategory',
list: canvasCategory,
label: 'Canvas category',
},
];
return filters;
}

View File

@ -17,13 +17,21 @@ import { useCallback } from 'react';
import { AgentCard } from './agent-card'; import { AgentCard } from './agent-card';
import { CreateAgentDialog } from './create-agent-dialog'; import { CreateAgentDialog } from './create-agent-dialog';
import { useCreateAgentOrPipeline } from './hooks/use-create-agent'; import { useCreateAgentOrPipeline } from './hooks/use-create-agent';
import { useSelectFilters } from './hooks/use-selelct-filters';
import { UploadAgentDialog } from './upload-agent-dialog'; import { UploadAgentDialog } from './upload-agent-dialog';
import { useHandleImportJsonFile } from './use-import-json'; import { useHandleImportJsonFile } from './use-import-json';
import { useRenameAgent } from './use-rename-agent'; import { useRenameAgent } from './use-rename-agent';
export default function Agents() { export default function Agents() {
const { data, pagination, setPagination, searchString, handleInputChange } = const {
useFetchAgentListByPage(); data,
pagination,
setPagination,
searchString,
handleInputChange,
filterValue,
handleFilterSubmit,
} = useFetchAgentListByPage();
const { navigateToAgentTemplates } = useNavigatePage(); const { navigateToAgentTemplates } = useNavigatePage();
const { const {
@ -50,6 +58,8 @@ export default function Agents() {
hideFileUploadModal, hideFileUploadModal,
} = useHandleImportJsonFile(); } = useHandleImportJsonFile();
const filters = useSelectFilters();
const handlePageChange = useCallback( const handlePageChange = useCallback(
(page: number, pageSize?: number) => { (page: number, pageSize?: number) => {
setPagination({ page, pageSize }); setPagination({ page, pageSize });
@ -65,6 +75,9 @@ export default function Agents() {
searchString={searchString} searchString={searchString}
onSearchChange={handleInputChange} onSearchChange={handleInputChange}
icon="agent" icon="agent"
filters={filters}
onChange={handleFilterSubmit}
value={filterValue}
> >
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>

View File

@ -31,12 +31,16 @@ export const FormSchema = z.object({
const SearchMethodOptions = buildOptions(TokenizerSearchMethod); const SearchMethodOptions = buildOptions(TokenizerSearchMethod);
const FieldsOptions = buildOptions(TokenizerFields);
const TokenizerForm = ({ node }: INextOperatorForm) => { const TokenizerForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation(); const { t } = useTranslation();
const defaultValues = useFormValues(initialTokenizerValues, node); const defaultValues = useFormValues(initialTokenizerValues, node);
const FieldsOptions = buildOptions(
TokenizerFields,
t,
'dataflow.tokenizerFieldsOptions',
);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
defaultValues, defaultValues,
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),

View File

@ -3,19 +3,19 @@ import { useCallback } from 'react';
export function useCancelCurrentDataflow({ export function useCancelCurrentDataflow({
messageId, messageId,
setMessageId, stopFetchTrace,
}: { }: {
messageId: string; messageId: string;
setMessageId: (messageId: string) => void; stopFetchTrace(): void;
}) { }) {
const { cancelDataflow } = useCancelDataflow(); const { cancelDataflow } = useCancelDataflow();
const handleCancel = useCallback(async () => { const handleCancel = useCallback(async () => {
const code = await cancelDataflow(messageId); const code = await cancelDataflow(messageId);
if (code === 0) { if (code === 0) {
setMessageId(''); stopFetchTrace();
} }
}, [cancelDataflow, messageId, setMessageId]); }, [cancelDataflow, messageId, stopFetchTrace]);
return { handleCancel }; return { handleCancel };
} }

View File

@ -1,10 +1,16 @@
import { useFetchMessageTrace } from '@/hooks/use-agent-request'; import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
export function useFetchLog() { export function useFetchLog(logSheetVisible: boolean) {
const { setMessageId, data, loading, messageId } = const {
useFetchMessageTrace(false); setMessageId,
data,
loading,
messageId,
setISStopFetchTrace,
isStopFetchTrace,
} = useFetchMessageTrace();
const isCompleted = useMemo(() => { const isCompleted = useMemo(() => {
if (Array.isArray(data)) { if (Array.isArray(data)) {
@ -13,18 +19,38 @@ export function useFetchLog() {
latest?.component_id === 'END' && !isEmpty(latest?.trace[0].message) latest?.component_id === 'END' && !isEmpty(latest?.trace[0].message)
); );
} }
return true; return false;
}, [data]); }, [data]);
const isLogEmpty = !data || !data.length; const isLogEmpty = !data || !data.length;
const stopFetchTrace = useCallback(() => {
setISStopFetchTrace(true);
}, [setISStopFetchTrace]);
// cancel request
useEffect(() => {
if (isCompleted) {
stopFetchTrace();
}
}, [isCompleted, stopFetchTrace]);
useEffect(() => {
if (logSheetVisible) {
setISStopFetchTrace(false);
}
}, [logSheetVisible, setISStopFetchTrace]);
return { return {
data, logs: data,
isLogEmpty, isLogEmpty,
isCompleted, isCompleted,
loading, loading,
isParsing: !isLogEmpty && !isCompleted, isParsing: !isLogEmpty && !isCompleted && !isStopFetchTrace,
messageId, messageId,
setMessageId, setMessageId,
stopFetchTrace,
}; };
} }
export type UseFetchLogReturnType = ReturnType<typeof useFetchLog>;

View File

@ -89,20 +89,29 @@ export default function DataFlow() {
hideModal: hideLogSheet, hideModal: hideLogSheet,
} = useSetModalState(); } = useSetModalState();
const { isParsing, data, messageId, setMessageId } = useFetchLog(); const {
isParsing,
logs,
messageId,
setMessageId,
isCompleted,
stopFetchTrace,
isLogEmpty,
} = useFetchLog(logSheetVisible);
const handleRunAgent = useCallback(() => { const handleRunAgent = useCallback(() => {
if (isParsing) { if (isParsing) {
// show log sheet // show log sheet
showLogSheet(); showLogSheet();
} else { } else {
hideLogSheet();
handleRun(); handleRun();
} }
}, [handleRun, isParsing, showLogSheet]); }, [handleRun, hideLogSheet, isParsing, showLogSheet]);
const { handleCancel } = useCancelCurrentDataflow({ const { handleCancel } = useCancelCurrentDataflow({
messageId, messageId,
setMessageId, stopFetchTrace,
}); });
const time = useWatchAgentChange(chatDrawerVisible); const time = useWatchAgentChange(chatDrawerVisible);
@ -139,7 +148,6 @@ export default function DataFlow() {
<ButtonLoading <ButtonLoading
variant={'secondary'} variant={'secondary'}
onClick={handleRunAgent} onClick={handleRunAgent}
disabled={isParsing}
loading={running} loading={running}
> >
{running || ( {running || (
@ -199,7 +207,9 @@ export default function DataFlow() {
<LogSheet <LogSheet
hideModal={hideLogSheet} hideModal={hideLogSheet}
isParsing={isParsing} isParsing={isParsing}
logs={data} isCompleted={isCompleted}
isLogEmpty={isLogEmpty}
logs={logs}
handleCancel={handleCancel} handleCancel={handleCancel}
></LogSheet> ></LogSheet>
)} )}

View File

@ -10,6 +10,7 @@ import {
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { ITraceData } from '@/interfaces/database/agent'; import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { isEmpty } from 'lodash';
import { File } from 'lucide-react'; import { File } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Operator } from '../constant'; import { Operator } from '../constant';
@ -82,7 +83,9 @@ export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
</div> </div>
</section> </section>
<div className="divide-y space-y-1"> <div className="divide-y space-y-1">
{traces.map((x, idx) => ( {traces
.filter((x) => !isEmpty(x.message))
.map((x, idx) => (
<section <section
key={idx} key={idx}
className="text-text-secondary text-xs space-x-2 py-2.5 !m-0" className="text-text-secondary text-xs space-x-2 py-2.5 !m-0"
@ -98,7 +101,9 @@ export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
{x.message} {x.message}
</span> </span>
)} )}
<span>{x.elapsed_time.toString().slice(0, 6)}s</span> <span>
{x.elapsed_time.toString().slice(0, 6)}s
</span>
</section> </section>
))} ))}
</div> </div>

View File

@ -1,3 +1,4 @@
import { SkeletonCard } from '@/components/skeleton-card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Sheet, Sheet,
@ -6,7 +7,6 @@ import {
SheetTitle, SheetTitle,
} from '@/components/ui/sheet'; } from '@/components/ui/sheet';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
ArrowUpRight, ArrowUpRight,
@ -20,19 +20,23 @@ import {
isEndOutputEmpty, isEndOutputEmpty,
useDownloadOutput, useDownloadOutput,
} from '../hooks/use-download-output'; } from '../hooks/use-download-output';
import { UseFetchLogReturnType } from '../hooks/use-fetch-log';
import { DataflowTimeline } from './dataflow-timeline'; import { DataflowTimeline } from './dataflow-timeline';
type LogSheetProps = IModalProps<any> & { type LogSheetProps = IModalProps<any> & {
isParsing: boolean;
handleCancel(): void; handleCancel(): void;
logs?: ITraceData[]; } & Pick<
}; UseFetchLogReturnType,
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs'
>;
export function LogSheet({ export function LogSheet({
hideModal, hideModal,
isParsing, isParsing,
logs, logs,
handleCancel, handleCancel,
isCompleted,
isLogEmpty,
}: LogSheetProps) { }: LogSheetProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -47,13 +51,17 @@ export function LogSheet({
<SheetHeader> <SheetHeader>
<SheetTitle className="flex items-center gap-2.5"> <SheetTitle className="flex items-center gap-2.5">
<Logs className="size-4" /> {t('flow.log')} <Logs className="size-4" /> {t('flow.log')}
<Button variant={'ghost'}> <Button variant={'ghost'} disabled={!isCompleted}>
{t('dataflow.viewResult')} <ArrowUpRight /> {t('dataflow.viewResult')} <ArrowUpRight />
</Button> </Button>
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<section className="max-h-[82vh] overflow-auto mt-6"> <section className="max-h-[82vh] overflow-auto mt-6">
{isLogEmpty ? (
<SkeletonCard className="mt-2" />
) : (
<DataflowTimeline traceList={logs}></DataflowTimeline> <DataflowTimeline traceList={logs}></DataflowTimeline>
)}
</section> </section>
{isParsing ? ( {isParsing ? (
<Button <Button

View File

@ -1,13 +1,10 @@
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox'; import { CheckedState } from '@radix-ui/react-checkbox';
import { useEffect, useState } from 'react';
import { ChunkTextMode } from '../../constant'; import { ChunkTextMode } from '../../constant';
import styles from '../../index.less'; import { ArrayContainer, parserKeyMap } from './json-parser';
import { ObjectContainer } from './object-parser';
interface FormatPreserveEditorProps { interface FormatPreserveEditorProps {
initialValue: { initialValue: {
key: string; key: keyof typeof parserKeyMap | 'text' | 'html';
type: string; type: string;
value: Array<{ [key: string]: string }>; value: Array<{ [key: string]: string }>;
}; };
@ -29,152 +26,47 @@ const FormatPreserveEditor = ({
selectedChunkIds, selectedChunkIds,
textMode, textMode,
}: FormatPreserveEditorProps) => { }: FormatPreserveEditorProps) => {
const [content, setContent] = useState(initialValue);
// const [isEditing, setIsEditing] = useState(false);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
undefined,
);
console.log('initialValue', initialValue); console.log('initialValue', initialValue);
useEffect(() => {
setContent(initialValue);
}, [initialValue]);
const handleEdit = (e?: any, index?: number) => {
console.log(e, index, content);
if (content.key === 'json') {
console.log(e, e.target.innerText);
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === index) {
return {
...item,
[Object.keys(item)[0]]: e.target.innerText,
};
}
return item;
}),
}));
setActiveEditIndex(index);
}
};
const handleChange = (e: any) => {
if (content.key === 'json') {
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === activeEditIndex) {
return {
...item,
[Object.keys(item)[0]]: e.target.value,
};
}
return item;
}),
}));
} else {
setContent(e.target.value);
}
};
const escapeNewlines = (text: string) => { const escapeNewlines = (text: string) => {
return text.replace(/\n/g, '\\n'); return text.replace(/\n/g, '\\n');
}; };
const unescapeNewlines = (text: string) => { const unescapeNewlines = (text: string) => {
return text.replace(/\\n/g, '\n'); return text.replace(/\\n/g, '\n');
}; };
const handleSave = () => {
const saveData = {
...content,
value: content.value?.map((item) => {
return { ...item, text: unescapeNewlines(item.text) };
}),
};
onSave(saveData);
setActiveEditIndex(undefined);
};
const handleCheck = (e: CheckedState, id: string | number) => { const handleCheck = (e: CheckedState, id: string | number) => {
handleCheckboxClick?.(id, e === 'indeterminate' ? false : e); handleCheckboxClick?.(id, e === 'indeterminate' ? false : e);
}; };
return ( return (
<div className="editor-container"> <div className="editor-container">
{/* {isEditing && content.key === 'json' ? ( {['json', 'chunks'].includes(initialValue.key) && (
<Textarea <ArrayContainer
className={cn( className={className}
'w-full h-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none min-h-6 p-0', initialValue={initialValue}
className, handleCheck={handleCheck}
)} selectedChunkIds={selectedChunkIds}
value={content.value} onSave={onSave}
onChange={handleChange} escapeNewlines={escapeNewlines}
onBlur={handleSave} unescapeNewlines={unescapeNewlines}
autoSize={{ maxRows: 100 }} textMode={textMode}
autoFocus isChunck={isChunck}
/>
) : (
<>
{content.key === 'json' && */}
{content.value?.map((item, index) => (
<section
key={index}
className={
isChunck
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
: ''
}
>
{isChunck && (
<Checkbox
onCheckedChange={(e) => {
handleCheck(e, index);
}}
checked={selectedChunkIds?.some(
(id) => id.toString() === index.toString(),
)}
></Checkbox>
)}
{activeEditIndex === index && (
<Textarea
key={'t' + index}
className={cn(
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none !h-6 min-h-6 p-0',
className,
)}
value={escapeNewlines(content.value[index].text)}
onChange={handleChange}
onBlur={handleSave}
autoSize={{ maxRows: 100, minRows: 1 }}
autoFocus
/> />
)} )}
{activeEditIndex !== index && (
<div {['text', 'html'].includes(initialValue.key) && (
className={cn( <ObjectContainer
'text-text-secondary overflow-auto scrollbar-auto whitespace-pre-wrap w-full', className={className}
{ initialValue={initialValue}
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse, handleCheck={handleCheck}
}, selectedChunkIds={selectedChunkIds}
onSave={onSave}
escapeNewlines={escapeNewlines}
unescapeNewlines={unescapeNewlines}
textMode={textMode}
isChunck={isChunck}
/>
)} )}
key={index}
onClick={(e) => {
handleEdit(e, index);
}}
>
{escapeNewlines(item.text)}
</div>
)}
</section>
))}
{/* {content.key !== 'json' && (
<pre
className="text-text-secondary overflow-auto scrollbar-auto"
onClick={handleEdit}
>
</pre>
)}
</>
)}*/}
</div> </div>
); );
}; };

View File

@ -0,0 +1,173 @@
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox';
import { useCallback, useEffect, useRef, useState } from 'react';
import { ChunkTextMode } from '../../constant';
import styles from '../../index.less';
export const parserKeyMap = {
json: 'text',
chunks: 'content_with_weight',
};
type IProps = {
initialValue: {
key: keyof typeof parserKeyMap;
type: string;
value: {
[key: string]: string;
}[];
};
isChunck?: boolean;
handleCheck: (e: CheckedState, index: number) => void;
selectedChunkIds: string[] | undefined;
unescapeNewlines: (text: string) => string;
escapeNewlines: (text: string) => string;
onSave: (data: {
value: {
text: string;
}[];
key: string;
type: string;
}) => void;
className?: string;
textMode?: ChunkTextMode;
};
export const ArrayContainer = (props: IProps) => {
const {
initialValue,
isChunck,
handleCheck,
selectedChunkIds,
unescapeNewlines,
escapeNewlines,
onSave,
className,
textMode,
} = props;
const [content, setContent] = useState(initialValue);
useEffect(() => {
setContent(initialValue);
console.log('initialValue json parse', initialValue);
}, [initialValue]);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
undefined,
);
const editDivRef = useRef<HTMLDivElement>(null);
const handleEdit = useCallback(
(e?: any, index?: number) => {
console.log(e, e.target.innerText);
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === index) {
return {
...item,
[parserKeyMap[content.key]]: e.target.innerText,
};
}
return item;
}),
}));
setActiveEditIndex(index);
},
[setContent, setActiveEditIndex],
);
const handleSave = useCallback(
(e: any) => {
console.log(e, e.target.innerText);
const saveData = {
...content,
value: content.value?.map((item, index) => {
if (index === activeEditIndex) {
return {
...item,
[parserKeyMap[content.key]]: unescapeNewlines(e.target.innerText),
};
} else {
return item;
}
}),
};
onSave(saveData);
setActiveEditIndex(undefined);
},
[content, onSave],
);
useEffect(() => {
if (activeEditIndex !== undefined && editDivRef.current) {
editDivRef.current.focus();
editDivRef.current.textContent =
content.value[activeEditIndex][parserKeyMap[content.key]];
}
}, [activeEditIndex, content]);
return (
<>
{content.value?.map((item, index) => {
if (item[parserKeyMap[content.key]] === '') {
return null;
}
return (
<section
key={index}
className={
isChunck
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
: ''
}
>
{isChunck && (
<Checkbox
onCheckedChange={(e) => {
handleCheck(e, index);
}}
checked={selectedChunkIds?.some(
(id) => id.toString() === index.toString(),
)}
></Checkbox>
)}
{activeEditIndex === index && (
<div
ref={editDivRef}
contentEditable={true}
onBlur={handleSave}
// onKeyUp={handleChange}
// dangerouslySetInnerHTML={{
// __html: DOMPurify.sanitize(
// escapeNewlines(
// content.value[index][parserKeyMap[content.key]],
// ),
// ),
// }}
className={cn(
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none p-0',
className,
)}
></div>
)}
{activeEditIndex !== index && (
<div
className={cn(
'text-text-secondary overflow-auto scrollbar-auto whitespace-pre-wrap w-full',
{
[styles.contentEllipsis]:
textMode === ChunkTextMode.Ellipse,
},
)}
key={index}
onClick={(e) => {
handleEdit(e, index);
}}
>
{escapeNewlines(item[parserKeyMap[content.key]])}
</div>
)}
</section>
);
})}
</>
);
};

View File

@ -0,0 +1,112 @@
import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox';
import { useCallback, useEffect, useRef, useState } from 'react';
import { ChunkTextMode } from '../../constant';
import styles from '../../index.less';
type IProps = {
initialValue: {
key: string;
type: string;
value: string;
};
isChunck?: boolean;
handleCheck: (e: CheckedState, index: number) => void;
unescapeNewlines: (text: string) => string;
escapeNewlines: (text: string) => string;
onSave: (data: { value: string; key: string; type: string }) => void;
className?: string;
textMode?: ChunkTextMode;
};
export const ObjectContainer = (props: IProps) => {
const {
initialValue,
isChunck,
unescapeNewlines,
escapeNewlines,
onSave,
className,
textMode,
} = props;
const [content, setContent] = useState(initialValue);
useEffect(() => {
setContent(initialValue);
console.log('initialValue object parse', initialValue);
}, [initialValue]);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
undefined,
);
const editDivRef = useRef<HTMLDivElement>(null);
const handleEdit = useCallback(
(e?: any) => {
console.log(e, e.target.innerText);
setContent((pre) => ({
...pre,
value: e.target.innerText,
}));
setActiveEditIndex(1);
},
[setContent, setActiveEditIndex],
);
const handleSave = useCallback(
(e: any) => {
console.log(e, e.target.innerText);
const saveData = {
...content,
value: unescapeNewlines(e.target.innerText),
};
onSave(saveData);
setActiveEditIndex(undefined);
},
[content, onSave],
);
useEffect(() => {
if (activeEditIndex !== undefined && editDivRef.current) {
editDivRef.current.focus();
editDivRef.current.textContent = escapeNewlines(content.value);
}
}, [activeEditIndex, content, escapeNewlines]);
return (
<>
<section
className={
isChunck
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
: ''
}
>
{activeEditIndex && (
<div
ref={editDivRef}
contentEditable={true}
onBlur={handleSave}
className={cn(
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none p-0',
className,
)}
/>
)}
{!activeEditIndex && (
<div
className={cn(
'text-text-secondary overflow-auto scrollbar-auto whitespace-pre-wrap w-full',
{
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse,
},
)}
onClick={(e) => {
handleEdit(e);
}}
>
{escapeNewlines(content.value)}
</div>
)}
</section>
</>
);
};

View File

@ -38,10 +38,6 @@ export const TimelineNodeObj = {
}, },
[TimelineNodeType.characterSplitter]: { [TimelineNodeType.characterSplitter]: {
title: 'Character Splitter', title: 'Character Splitter',
icon: <Heading size={13} />,
},
[TimelineNodeType.splitter]: {
title: 'Splitter',
icon: <Blocks size={13} />, icon: <Blocks size={13} />,
}, },
[TimelineNodeType.tokenizer]: { [TimelineNodeType.tokenizer]: {

View File

@ -6,10 +6,9 @@ export enum ChunkTextMode {
export enum TimelineNodeType { export enum TimelineNodeType {
begin = 'file', begin = 'file',
parser = 'parser', parser = 'parser',
splitter = 'splitter', contextGenerator = 'extractor',
contextGenerator = 'contextGenerator', titleSplitter = 'hierarchicalMerger',
titleSplitter = 'titleSplitter', characterSplitter = 'splitter',
characterSplitter = 'characterSplitter', tokenizer = 'indexer',
tokenizer = 'tokenizer',
end = 'end', end = 'end',
} }

View File

@ -1,4 +1,5 @@
import { TimelineNode } from '@/components/originui/timeline'; import { TimelineNode } from '@/components/originui/timeline';
import message from '@/components/ui/message';
import { import {
useCreateChunk, useCreateChunk,
useDeleteChunk, useDeleteChunk,
@ -10,7 +11,8 @@ import { IChunk } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service'; import kbService from '@/services/knowledge-service';
import { formatSecondsToHumanReadable } from '@/utils/date'; import { formatSecondsToHumanReadable } from '@/utils/date';
import { buildChunkHighlights } from '@/utils/document-util'; import { buildChunkHighlights } from '@/utils/document-util';
import { useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { t } from 'i18next';
import { camelCase, upperFirst } from 'lodash'; import { camelCase, upperFirst } from 'lodash';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { IHighlight } from 'react-pdf-highlighter'; import { IHighlight } from 'react-pdf-highlighter';
@ -169,22 +171,16 @@ export const useUpdateChunk = () => {
}; };
}; };
export const useFetchParserList = () => {
const [loading, setLoading] = useState(false);
return {
loading,
};
};
export const useRerunDataflow = ({ export const useRerunDataflow = ({
data, data,
}: { }: {
data: IPipelineFileLogDetail; data: IPipelineFileLogDetail;
}) => { }) => {
const [loading, setLoading] = useState(false);
const [isChange, setIsChange] = useState(false); const [isChange, setIsChange] = useState(false);
const handleReRunFunc = useCallback(
(newData: { value: IDslComponent; key: string }) => { const { mutateAsync: handleReRunFunc, isPending: loading } = useMutation({
mutationKey: ['pipelineRerun', data],
mutationFn: async (newData: { value: IDslComponent; key: string }) => {
const newDsl = { const newDsl = {
...data.dsl, ...data.dsl,
components: { components: {
@ -197,16 +193,21 @@ export const useRerunDataflow = ({
const params = { const params = {
id: data.id, id: data.id,
dsl: newDsl, dsl: newDsl,
compenent_id: newData.key, component_id: newData.key,
}; };
console.log('newDsl', newDsl, params); const { data: result } = await kbService.pipelineRerun(params);
if (result.code === 0) {
message.success(t('message.operated'));
// queryClient.invalidateQueries({
// queryKey: [type],
// });
}
return result;
}, },
[data], });
);
return { return {
loading, loading,
setLoading,
isChange, isChange,
setIsChange, setIsChange,
handleReRunFunc, handleReRunFunc,
@ -225,7 +226,6 @@ export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
type: type:
| TimelineNodeType.begin | TimelineNodeType.begin
| TimelineNodeType.parser | TimelineNodeType.parser
| TimelineNodeType.splitter
| TimelineNodeType.tokenizer | TimelineNodeType.tokenizer
| TimelineNodeType.characterSplitter | TimelineNodeType.characterSplitter
| TimelineNodeType.titleSplitter, | TimelineNodeType.titleSplitter,
@ -242,10 +242,9 @@ export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
tempType = TimelineNodeType.tokenizer; tempType = TimelineNodeType.tokenizer;
} else if ( } else if (
name === TimelineNodeType.characterSplitter || name === TimelineNodeType.characterSplitter ||
name === TimelineNodeType.titleSplitter || name === TimelineNodeType.titleSplitter
name === TimelineNodeType.splitter
) { ) {
tempType = TimelineNodeType.splitter; tempType = TimelineNodeType.characterSplitter;
} }
const timeNode = { const timeNode = {
...TimelineNodeObj[name], ...TimelineNodeObj[name],

View File

@ -94,18 +94,18 @@ const Chunk = () => {
></div> ></div>
), ),
onVisibleChange: () => { onVisibleChange: () => {
Modal.hide(); Modal.destroy();
}, },
footer: ( footer: (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant={'outline'} onClick={() => Modal.hide()}> <Button variant={'outline'} onClick={() => Modal.destroy()}>
{t('dataflowParser.changeStepModalCancelText')} {t('dataflowParser.changeStepModalCancelText')}
</Button> </Button>
<Button <Button
variant={'secondary'} variant={'secondary'}
className="!bg-state-error text-text-primary" className="!bg-state-error text-text-primary"
onClick={() => { onClick={() => {
Modal.hide(); Modal.destroy();
setActiveStepId(id); setActiveStepId(id);
setIsChange(false); setIsChange(false);
}} }}
@ -193,7 +193,8 @@ const Chunk = () => {
)} */} )} */}
{/* {currentTimeNode?.type === TimelineNodeType.parser && ( */} {/* {currentTimeNode?.type === TimelineNodeType.parser && ( */}
{(currentTimeNode?.type === TimelineNodeType.parser || {(currentTimeNode?.type === TimelineNodeType.parser ||
currentTimeNode?.type === TimelineNodeType.splitter) && ( currentTimeNode?.type === TimelineNodeType.characterSplitter ||
currentTimeNode?.type === TimelineNodeType.titleSplitter) && (
<ParserContainer <ParserContainer
isChange={isChange} isChange={isChange}
reRunLoading={reRunLoading} reRunLoading={reRunLoading}

View File

@ -1,16 +1,15 @@
import { TimelineNode } from '@/components/originui/timeline'; import { TimelineNode } from '@/components/originui/timeline';
import Spotlight from '@/components/spotlight'; import Spotlight from '@/components/spotlight';
import { Spin } from '@/components/ui/spin';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ChunkResultBar from './components/chunk-result-bar'; import ChunkResultBar from './components/chunk-result-bar';
import CheckboxSets from './components/chunk-result-bar/checkbox-sets'; import CheckboxSets from './components/chunk-result-bar/checkbox-sets';
import FormatPreserEditor from './components/parse-editer'; import FormatPreserEditor from './components/parse-editer';
import RerunButton from './components/rerun-button'; import RerunButton from './components/rerun-button';
import { TimelineNodeType } from './constant'; import { TimelineNodeType } from './constant';
import { useChangeChunkTextMode, useFetchParserList } from './hooks'; import { useChangeChunkTextMode } from './hooks';
import { IDslComponent } from './interface'; import { IDslComponent } from './interface';
interface IProps { interface IProps {
isChange: boolean; isChange: boolean;
@ -23,15 +22,15 @@ interface IProps {
const ParserContainer = (props: IProps) => { const ParserContainer = (props: IProps) => {
const { isChange, setIsChange, step, data, reRunFunc, reRunLoading } = props; const { isChange, setIsChange, step, data, reRunFunc, reRunLoading } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { loading } = useFetchParserList();
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); const { changeChunkTextMode, textMode } = useChangeChunkTextMode();
const initialValue = useMemo(() => { const initialValue = useMemo(() => {
const outputs = data?.value?.obj?.params?.outputs; const outputs = data?.value?.obj?.params?.outputs;
const key = outputs?.output_format?.value; const key = outputs?.output_format?.value;
if (!outputs || !key) return { key: '', type: '', value: [] };
const value = outputs[key]?.value; const value = outputs[key]?.value;
const type = outputs[key]?.type; const type = outputs[key]?.type;
console.log('outputs-->', outputs); console.log('outputs-->', outputs, data, key, value);
return { return {
key, key,
type, type,
@ -40,6 +39,10 @@ const ParserContainer = (props: IProps) => {
}, [data]); }, [data]);
const [initialText, setInitialText] = useState(initialValue); const [initialText, setInitialText] = useState(initialValue);
useEffect(() => {
setInitialText(initialValue);
}, [initialValue]);
const handleSave = (newContent: any) => { const handleSave = (newContent: any) => {
console.log('newContent-change-->', newContent, initialValue); console.log('newContent-change-->', newContent, initialValue);
if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) { if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) {
@ -109,8 +112,7 @@ const ParserContainer = (props: IProps) => {
const isChunck = const isChunck =
step?.type === TimelineNodeType.characterSplitter || step?.type === TimelineNodeType.characterSplitter ||
step?.type === TimelineNodeType.titleSplitter || step?.type === TimelineNodeType.titleSplitter;
step?.type === TimelineNodeType.splitter;
const handleCreateChunk = useCallback( const handleCreateChunk = useCallback(
(text: string) => { (text: string) => {
@ -124,6 +126,7 @@ const ParserContainer = (props: IProps) => {
}, },
[initialText], [initialText],
); );
return ( return (
<> <>
{isChange && ( {isChange && (
@ -136,7 +139,7 @@ const ParserContainer = (props: IProps) => {
</div> </div>
)} )}
<div className={classNames('flex flex-col w-full')}> <div className={classNames('flex flex-col w-full')}>
<Spin spinning={loading} className="" size="large"> {/* <Spin spinning={false} className="" size="large"> */}
<div className="h-[50px] flex flex-col justify-end pb-[5px]"> <div className="h-[50px] flex flex-col justify-end pb-[5px]">
{!isChunck && ( {!isChunck && (
<div> <div>
@ -182,25 +185,26 @@ const ParserContainer = (props: IProps) => {
}, },
)} )}
> >
{initialText && (
<FormatPreserEditor <FormatPreserEditor
initialValue={initialText} initialValue={initialText}
onSave={handleSave} onSave={handleSave}
className={ // className={
initialText.key !== 'json' ? '!h-[calc(100vh-220px)]' : '' // initialText.key !== 'json' ? '!h-[calc(100vh-220px)]' : ''
} // }
isChunck={isChunck} isChunck={isChunck}
textMode={textMode} textMode={textMode}
isDelete={ isDelete={
step?.type === TimelineNodeType.characterSplitter || step?.type === TimelineNodeType.characterSplitter ||
step?.type === TimelineNodeType.titleSplitter || step?.type === TimelineNodeType.titleSplitter
step?.type === TimelineNodeType.splitter
} }
handleCheckboxClick={handleCheckboxClick} handleCheckboxClick={handleCheckboxClick}
selectedChunkIds={selectedChunkIds} selectedChunkIds={selectedChunkIds}
/> />
)}
<Spotlight opcity={0.6} coverage={60} /> <Spotlight opcity={0.6} coverage={60} />
</div> </div>
</Spin> {/* </Spin> */}
</div> </div>
</> </>
); );

View File

@ -12,7 +12,10 @@ import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { TopTitle } from '../dataset-title'; import { TopTitle } from '../dataset-title';
import { IGenerateLogButtonProps } from '../dataset/generate-button/generate'; import {
GenerateType,
IGenerateLogButtonProps,
} from '../dataset/generate-button/generate';
import LinkDataPipeline, { import LinkDataPipeline, {
IDataPipelineNodeProps, IDataPipelineNodeProps,
} from './components/link-data-pipeline'; } from './components/link-data-pipeline';
@ -120,6 +123,20 @@ export default function DatasetSettings() {
// form.setValue('pipeline_avatar', data.avatar || ''); // form.setValue('pipeline_avatar', data.avatar || '');
} }
}; };
const handleDeletePipelineTask = (type: GenerateType) => {
if (type === GenerateType.KnowledgeGraph) {
setGraphRagGenerateData({
finish_at: '',
task_id: '',
} as IGenerateLogButtonProps);
} else if (type === GenerateType.Raptor) {
setRaptorGenerateData({
finish_at: '',
task_id: '',
} as IGenerateLogButtonProps);
}
};
return ( return (
<section className="p-5 h-full flex flex-col"> <section className="p-5 h-full flex flex-col">
<TopTitle <TopTitle
@ -140,10 +157,14 @@ export default function DatasetSettings() {
<GraphRagItems <GraphRagItems
className="border-none p-0" className="border-none p-0"
data={graphRagGenerateData as IGenerateLogButtonProps} data={graphRagGenerateData as IGenerateLogButtonProps}
onDelete={() =>
handleDeletePipelineTask(GenerateType.KnowledgeGraph)
}
></GraphRagItems> ></GraphRagItems>
<Divider /> <Divider />
<RaptorFormFields <RaptorFormFields
data={raptorGenerateData as IGenerateLogButtonProps} data={raptorGenerateData as IGenerateLogButtonProps}
onDelete={() => handleDeletePipelineTask(GenerateType.Raptor)}
></RaptorFormFields> ></RaptorFormFields>
<Divider /> <Divider />
<LinkDataPipeline <LinkDataPipeline

View File

@ -16,17 +16,23 @@ import { lowerFirst } from 'lodash';
import { CirclePause, Trash2, WandSparkles } from 'lucide-react'; import { CirclePause, Trash2, WandSparkles } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ProcessingType } from '../../dataset-overview/dataset-common';
import { replaceText } from '../../process-log-modal'; import { replaceText } from '../../process-log-modal';
import { import {
ITraceInfo, ITraceInfo,
generateStatus, generateStatus,
useDatasetGenerate, useDatasetGenerate,
useTraceGenerate, useTraceGenerate,
useUnBindTask,
} from './hook'; } from './hook';
export enum GenerateType { export enum GenerateType {
KnowledgeGraph = 'KnowledgeGraph', KnowledgeGraph = 'KnowledgeGraph',
Raptor = 'Raptor', Raptor = 'Raptor',
} }
export const GenerateTypeMap = {
[GenerateType.KnowledgeGraph]: ProcessingType.knowledgeGraph,
[GenerateType.Raptor]: ProcessingType.raptor,
};
const MenuItem: React.FC<{ const MenuItem: React.FC<{
name: GenerateType; name: GenerateType;
data: ITraceInfo; data: ITraceInfo;
@ -78,9 +84,11 @@ const MenuItem: React.FC<{
'border cursor-pointer p-2 rounded-md focus:bg-transparent', 'border cursor-pointer p-2 rounded-md focus:bg-transparent',
{ {
'hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]': 'hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]':
status === generateStatus.start, status === generateStatus.start ||
status === generateStatus.completed,
'hover:border-border hover:bg-[rgba(59,160,92,0)]': 'hover:border-border hover:bg-[rgba(59,160,92,0)]':
status !== generateStatus.start, status !== generateStatus.start &&
status !== generateStatus.completed,
}, },
)} )}
onSelect={(e) => { onSelect={(e) => {
@ -93,7 +101,10 @@ const MenuItem: React.FC<{
<div <div
className="flex items-start gap-2 flex-col w-full" className="flex items-start gap-2 flex-col w-full"
onClick={() => { onClick={() => {
if (status === generateStatus.start) { if (
status === generateStatus.start ||
status === generateStatus.completed
) {
runGenerate({ type }); runGenerate({ type });
} }
}} }}
@ -234,7 +245,21 @@ export type IGenerateLogProps = IGenerateLogButtonProps & {
}; };
export const GenerateLogButton = (props: IGenerateLogProps) => { export const GenerateLogButton = (props: IGenerateLogProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { task_id, message, finish_at, type, onDelete } = props; const { message, finish_at, type, onDelete } = props;
const { handleUnbindTask } = useUnBindTask();
const handleDeleteFunc = async () => {
const data = await handleUnbindTask({
type: GenerateTypeMap[type as GenerateType],
});
Modal.destroy();
console.log('handleUnbindTask', data);
if (data.code === 0) {
onDelete?.();
}
};
const handleDelete = () => { const handleDelete = () => {
Modal.show({ Modal.show({
visible: true, visible: true,
@ -259,14 +284,14 @@ export const GenerateLogButton = (props: IGenerateLogProps) => {
></div> ></div>
), ),
onVisibleChange: () => { onVisibleChange: () => {
Modal.hide(); Modal.destroy();
}, },
footer: ( footer: (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
type="button" type="button"
variant={'outline'} variant={'outline'}
onClick={() => Modal.hide()} onClick={() => Modal.destroy()}
> >
{t('dataflowParser.changeStepModalCancelText')} {t('dataflowParser.changeStepModalCancelText')}
</Button> </Button>
@ -275,7 +300,7 @@ export const GenerateLogButton = (props: IGenerateLogProps) => {
variant={'secondary'} variant={'secondary'}
className="!bg-state-error text-text-primary" className="!bg-state-error text-text-primary"
onClick={() => { onClick={() => {
Modal.hide(); handleDeleteFunc();
}} }}
> >
{t('common.delete')} {t('common.delete')}
@ -284,6 +309,7 @@ export const GenerateLogButton = (props: IGenerateLogProps) => {
), ),
}); });
}; };
return ( return (
<div <div
className={cn('flex bg-bg-card rounded-md py-1 px-3', props.className)} className={cn('flex bg-bg-card rounded-md py-1 px-3', props.className)}

View File

@ -1,10 +1,11 @@
import message from '@/components/ui/message'; import message from '@/components/ui/message';
import agentService from '@/services/agent-service'; import agentService from '@/services/agent-service';
import kbService from '@/services/knowledge-service'; import kbService, { deletePipelineTask } from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next'; import { t } from 'i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { ProcessingType } from '../../dataset-overview/dataset-common';
import { GenerateType } from './generate'; import { GenerateType } from './generate';
export const generateStatus = { export const generateStatus = {
running: 'running', running: 'running',
@ -153,3 +154,21 @@ export const useDatasetGenerate = () => {
}); });
return { runGenerate: mutateAsync, pauseGenerate, data, loading }; return { runGenerate: mutateAsync, pauseGenerate, data, loading };
}; };
export const useUnBindTask = () => {
const { id } = useParams();
const { mutateAsync: handleUnbindTask } = useMutation({
mutationKey: [DatasetKey.pauseGenerate],
mutationFn: async ({ type }: { type: ProcessingType }) => {
const { data } = await deletePipelineTask({ kb_id: id as string, type });
if (data.code === 0) {
message.success(t('message.operated'));
// queryClient.invalidateQueries({
// queryKey: [type],
// });
}
return data;
},
});
return { handleUnbindTask };
};

View File

@ -79,7 +79,7 @@ export const useShowLog = (documents: IDocumentInfo[]) => {
if (findRecord) { if (findRecord) {
log = { log = {
fileType: findRecord?.suffix, fileType: findRecord?.suffix,
uploadedBy: findRecord?.created_by, uploadedBy: findRecord?.nickname,
fileName: findRecord?.name, fileName: findRecord?.name,
uploadDate: formatDate(findRecord.create_date), uploadDate: formatDate(findRecord.create_date),
fileSize: formatBytes(findRecord.size || 0), fileSize: formatBytes(findRecord.size || 0),

View File

@ -1,18 +1,11 @@
import { FilterCollection } from '@/components/list-filter-bar/interface'; import { FilterCollection } from '@/components/list-filter-bar/interface';
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; import { useFetchKnowledgeList } from '@/hooks/use-knowledge-request';
import { groupListByType } from '@/utils/dataset-util'; import { buildOwnersFilter } from '@/utils/list-filter-util';
import { useMemo } from 'react';
export function useSelectOwners() { export function useSelectOwners() {
const { list } = useFetchKnowledgeList(); const { list } = useFetchKnowledgeList();
const owners = useMemo(() => { const filters: FilterCollection[] = [buildOwnersFilter(list)];
return groupListByType(list, 'tenant_id', 'nickname');
}, [list]);
const filters: FilterCollection[] = [
{ field: 'owner', list: owners, label: 'Owner' },
];
return filters; return filters;
} }

View File

@ -17,7 +17,6 @@ const {
testDbConnect, testDbConnect,
getInputElements, getInputElements,
debug, debug,
listCanvasTeam,
settingCanvas, settingCanvas,
uploadCanvasFile, uploadCanvasFile,
trace, trace,
@ -85,10 +84,6 @@ const methods = {
url: debug, url: debug,
method: 'post', method: 'post',
}, },
listCanvasTeam: {
url: listCanvasTeam,
method: 'get',
},
settingCanvas: { settingCanvas: {
url: settingCanvas, url: settingCanvas,
method: 'post', method: 'post',

View File

@ -4,6 +4,7 @@ import {
IFetchKnowledgeListRequestBody, IFetchKnowledgeListRequestBody,
IFetchKnowledgeListRequestParams, IFetchKnowledgeListRequestParams,
} from '@/interfaces/request/knowledge'; } from '@/interfaces/request/knowledge';
import { ProcessingType } from '@/pages/dataset/dataset-overview/dataset-common';
import api from '@/utils/api'; import api from '@/utils/api';
import registerServer from '@/utils/register-server'; import registerServer from '@/utils/register-server';
import request, { post } from '@/utils/request'; import request, { post } from '@/utils/request';
@ -254,4 +255,14 @@ export const listPipelineDatasetLogs = (
body?: IFetchDocumentListRequestBody, body?: IFetchDocumentListRequestBody,
) => request.post(api.fetchPipelineDatasetLogs, { data: body || {}, params }); ) => request.post(api.fetchPipelineDatasetLogs, { data: body || {}, params });
export function deletePipelineTask({
kb_id,
type,
}: {
kb_id: string;
type: ProcessingType;
}) {
return request.delete(api.unbindPipelineTask({ kb_id, type }));
}
export default kbService; export default kbService;

View File

@ -54,6 +54,9 @@ export default {
traceGraphRag: `${api_host}/kb/trace_graphrag`, traceGraphRag: `${api_host}/kb/trace_graphrag`,
runRaptor: `${api_host}/kb/run_raptor`, runRaptor: `${api_host}/kb/run_raptor`,
traceRaptor: `${api_host}/kb/trace_raptor`, traceRaptor: `${api_host}/kb/trace_raptor`,
unbindPipelineTask: ({ kb_id, type }: { kb_id: string; type: string }) =>
`${api_host}/kb/unbind_task?kb_id=${kb_id}&pipeline_task_type=${type}`,
pipelineRerun: `${api_host}/canvas/rerun`,
// tags // tags
listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`,
@ -147,7 +150,6 @@ export default {
// flow // flow
listTemplates: `${api_host}/canvas/templates`, listTemplates: `${api_host}/canvas/templates`,
listCanvas: `${api_host}/canvas/list`, listCanvas: `${api_host}/canvas/list`,
listCanvasTeam: `${api_host}/canvas/list`,
getCanvas: `${api_host}/canvas/get`, getCanvas: `${api_host}/canvas/get`,
getCanvasSSE: `${api_host}/canvas/getsse`, getCanvasSSE: `${api_host}/canvas/getsse`,
removeCanvas: `${api_host}/canvas/rm`, removeCanvas: `${api_host}/canvas/rm`,

View File

@ -7,27 +7,3 @@ export function isKnowledgeGraphParser(parserId: DocumentParserType) {
export function isNaiveParser(parserId: DocumentParserType) { export function isNaiveParser(parserId: DocumentParserType) {
return parserId === DocumentParserType.Naive; return parserId === DocumentParserType.Naive;
} }
export type FilterType = {
id: string;
label: string;
count: number;
};
export function groupListByType<T extends Record<string, any>>(
list: T[],
idField: string,
labelField: string,
) {
const fileTypeList: FilterType[] = [];
list.forEach((x) => {
const item = fileTypeList.find((y) => y.id === x[idField]);
if (!item) {
fileTypeList.push({ id: x[idField], label: x[labelField], count: 1 });
} else {
item.count += 1;
}
});
return fileTypeList;
}

View File

@ -0,0 +1,29 @@
export type FilterType = {
id: string;
label: string;
count: number;
};
export function groupListByType<T extends Record<string, any>>(
list: T[],
idField: string,
labelField: string,
) {
const fileTypeList: FilterType[] = [];
list.forEach((x) => {
const item = fileTypeList.find((y) => y.id === x[idField]);
if (!item) {
fileTypeList.push({ id: x[idField], label: x[labelField], count: 1 });
} else {
item.count += 1;
}
});
return fileTypeList;
}
export function buildOwnersFilter<T extends Record<string, any>>(list: T[]) {
const owners = groupListByType(list, 'tenant_id', 'nickname');
return { field: 'owner', list: owners, label: 'Owner' };
}