瀏覽代碼

Feat: Add agent log-sheet in cavas and log-sheet in share's page (#9072)

### What problem does this PR solve?

Feat: Add agent log-sheet in cavas and log-sheet in share's page #3221 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.0
chanx 3 月之前
父節點
當前提交
381f9df941
沒有連結到貢獻者的電子郵件帳戶。

+ 22
- 0
web/src/components/next-message-item/index.tsx 查看文件

@@ -17,7 +17,9 @@ import {
useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { INodeEvent } from '@/hooks/use-send-message';
import { cn } from '@/lib/utils';
import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workFlowTimeline';
import { IMessage } from '@/pages/chat/interface';
import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
@@ -38,6 +40,9 @@ interface IProps
IRegenerateMessage,
PropsWithChildren {
item: IMessage;
conversationId?: string;
currentEventListWithoutMessageById?: (messageId: string) => INodeEvent[];
setCurrentMessageId?: (messageId: string) => void;
reference?: IReferenceObject;
loading?: boolean;
sendLoading?: boolean;
@@ -54,6 +59,9 @@ interface IProps

function MessageItem({
item,
conversationId,
currentEventListWithoutMessageById,
setCurrentMessageId,
reference,
loading = false,
avatar,
@@ -106,6 +114,11 @@ function MessageItem({
}
}, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);

useEffect(() => {
if (typeof setCurrentMessageId === 'function') {
setCurrentMessageId(item.id);
}
}, [item.id, setCurrentMessageId]);
return (
<div
className={classNames(styles.messageItem, {
@@ -186,6 +199,15 @@ function MessageItem({
list={referenceDocuments}
></ReferenceDocumentList>
)}
{isAssistant && currentEventListWithoutMessageById && (
<WorkFlowTimeline
currentEventListWithoutMessage={currentEventListWithoutMessageById(
item.id,
)}
currentMessageId={item.id}
canvasId={conversationId}
/>
)}
{isUser && documentList.length > 0 && (
<List
bordered

+ 11
- 7
web/src/hooks/use-agent-request.ts 查看文件

@@ -7,7 +7,7 @@ import { IDebugSingleRequestBody } from '@/interfaces/request/agent';
import i18n from '@/locales/config';
import { BeginId } from '@/pages/agent/constant';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import agentService from '@/services/agent-service';
import agentService, { fetchTrace } from '@/services/agent-service';
import api from '@/utils/api';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@@ -390,8 +390,12 @@ export const useUploadCanvasFileWithProgress = (
return { data, loading, uploadCanvasFile: mutateAsync };
};

export const useFetchMessageTrace = () => {
export const useFetchMessageTrace = (
isStopFetchTrace: boolean,
canvasId?: string,
) => {
const { id } = useParams();
const queryId = id || canvasId;
const [messageId, setMessageId] = useState('');

const {
@@ -399,16 +403,16 @@ export const useFetchMessageTrace = () => {
isFetching: loading,
refetch,
} = useQuery<ITraceData[]>({
queryKey: [AgentApiAction.Trace, id, messageId],
queryKey: [AgentApiAction.Trace, queryId, messageId],
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
enabled: !!id && !!messageId,
refetchInterval: 3000,
enabled: !!queryId && !!messageId,
refetchInterval: !isStopFetchTrace ? 3000 : false,
queryFn: async () => {
const { data } = await agentService.trace({
canvas_id: id,
const { data } = await fetchTrace({
canvas_id: queryId as string,
message_id: messageId,
});


+ 2
- 0
web/src/hooks/use-send-message.ts 查看文件

@@ -29,6 +29,8 @@ export interface INodeData {
inputs: Record<string, any>;
outputs: Record<string, any>;
component_id: string;
component_name: string;
component_type: string;
error: null | string;
elapsed_time: number;
created_at: number;

+ 4
- 2
web/src/pages/agent/canvas/index.tsx 查看文件

@@ -124,7 +124,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
const {
addEventList,
setCurrentMessageId,
currentEventListWithoutMessage,
currentEventListWithoutMessageById,
clearEventList,
currentMessageId,
} = useCacheChatLog();
@@ -258,7 +258,9 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
{logSheetVisible && (
<LogSheet
hideModal={hideLogSheet}
currentEventListWithoutMessage={currentEventListWithoutMessage}
currentEventListWithoutMessageById={
currentEventListWithoutMessageById
}
currentMessageId={currentMessageId}
></LogSheet>
)}

+ 13
- 4
web/src/pages/agent/chat/use-send-agent-message.ts 查看文件

@@ -169,13 +169,20 @@ export function useSetUploadResponseData() {
};
}

export const useSendAgentMessage = (url?: string) => {
export const useSendAgentMessage = (
url?: string,
addEventList?: (data: IEventList, messageId: string) => void,
) => {
const { id: agentId } = useParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const inputs = useSelectBeginNodeDataInputs();
const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE(
url || api.runCanvas,
);
const messageId = useMemo(() => {
return answerList[0]?.message_id;
}, [answerList]);

const { findReferenceByMessageId } = useFindMessageReference(answerList);
const prologue = useGetBeginNodePrologue();
const {
@@ -186,7 +193,7 @@ export const useSendAgentMessage = (url?: string) => {
addNewestOneQuestion,
addNewestOneAnswer,
} = useSelectDerivedMessages();
const { addEventList } = useContext(AgentChatLogContext);
const { addEventList: addEventListFun } = useContext(AgentChatLogContext);
const {
appendUploadResponseList,
clearUploadResponseList,
@@ -287,9 +294,11 @@ export const useSendAgentMessage = (url?: string) => {

useEffect(() => {
if (typeof addEventList === 'function') {
addEventList(answerList);
addEventList(answerList, messageId);
} else if (typeof addEventListFun === 'function') {
addEventListFun(answerList, messageId);
}
}, [addEventList, answerList]);
}, [addEventList, answerList, addEventListFun, messageId]);

return {
value,

+ 39
- 14
web/src/pages/agent/hooks/use-cache-chat-log.ts 查看文件

@@ -3,7 +3,7 @@ import {
INodeEvent,
MessageEventType,
} from '@/hooks/use-send-message';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';

export const ExcludeTypes = [
MessageEventType.Message,
@@ -12,51 +12,76 @@ export const ExcludeTypes = [

export function useCacheChatLog() {
const [eventList, setEventList] = useState<IEventList>([]);
const [messageIdPool, setMessageIdPool] = useState<
Record<string, IEventList>
>({});

const [currentMessageId, setCurrentMessageId] = useState('');
useEffect(() => {
setMessageIdPool((prev) => ({ ...prev, [currentMessageId]: eventList }));
}, [currentMessageId, eventList]);

const filterEventListByMessageId = useCallback(
(messageId: string) => {
return eventList.filter((x) => x.message_id === messageId);
return messageIdPool[messageId]?.filter(
(x) => x.message_id === messageId,
);
},
[eventList],
[messageIdPool],
);

const filterEventListByEventType = useCallback(
(eventType: string) => {
return eventList.filter((x) => x.event === eventType);
return messageIdPool[currentMessageId]?.filter(
(x) => x.event === eventType,
);
},
[eventList],
[messageIdPool, currentMessageId],
);

const clearEventList = useCallback(() => {
setEventList([]);
}, []);

const addEventList = useCallback((events: IEventList) => {
setEventList((list) => {
const nextList = [...list];
const addEventList = useCallback(
(events: IEventList, message_id: string) => {
const nextList = [...eventList];
events.forEach((x) => {
if (nextList.every((y) => y !== x)) {
nextList.push(x);
}
});
return nextList;
});
}, []);
setEventList(nextList);
setMessageIdPool((prev) => ({ ...prev, [message_id]: nextList }));
},
[eventList],
);

const currentEventListWithoutMessage = useMemo(() => {
const list = eventList.filter(
const list = messageIdPool[currentMessageId]?.filter(
(x) =>
x.message_id === currentMessageId &&
ExcludeTypes.every((y) => y !== x.event),
);

return list as INodeEvent[];
}, [currentMessageId, eventList]);
}, [currentMessageId, messageIdPool]);

const currentEventListWithoutMessageById = useCallback(
(messageId: string) => {
const list = messageIdPool[messageId]?.filter(
(x) =>
x.message_id === messageId &&
ExcludeTypes.every((y) => y !== x.event),
);
return list as INodeEvent[];
},
[messageIdPool],
);

return {
eventList,
currentEventListWithoutMessage,
currentEventListWithoutMessageById,
setEventList,
clearEventList,
addEventList,

+ 9
- 223
web/src/pages/agent/log-sheet/index.tsx 查看文件

@@ -1,156 +1,26 @@
import {
Timeline,
TimelineContent,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
} from '@/components/originui/timeline';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import {
INodeData,
INodeEvent,
MessageEventType,
} from '@/hooks/use-send-message';
import { IModalProps } from '@/interfaces/common';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { get } from 'lodash';
import { NotebookText } from 'lucide-react';
import { useCallback, useEffect, useMemo } from 'react';
import JsonView from 'react18-json-view';
import 'react18-json-view/src/style.css';
import { Operator } from '../constant';
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
import OperatorIcon from '../operator-icon';
import useGraphStore from '../store';
import { WorkFlowTimeline } from './workFlowTimeline';

type LogSheetProps = IModalProps<any> &
Pick<
ReturnType<typeof useCacheChatLog>,
'currentEventListWithoutMessage' | 'currentMessageId'
'currentEventListWithoutMessageById' | 'currentMessageId'
>;

function JsonViewer({
data,
title,
}: {
data: Record<string, any>;
title: string;
}) {
return (
<section className="space-y-2">
<div>{title}</div>
<JsonView
src={data}
displaySize
collapseStringsAfterLength={100000000000}
className="w-full h-[200px] break-words overflow-auto p-2 bg-slate-800"
/>
</section>
);
}

function getInputsOrOutputs(
nodeEventList: INodeData[],
field: 'inputs' | 'outputs',
) {
const inputsOrOutputs = nodeEventList.map((x) => get(x, field, {}));

if (inputsOrOutputs.length < 2) {
return inputsOrOutputs[0] || {};
}

return inputsOrOutputs;
}

export function LogSheet({
hideModal,
currentEventListWithoutMessage,
currentEventListWithoutMessageById,
currentMessageId,
}: LogSheetProps) {
const getNode = useGraphStore((state) => state.getNode);

const { data: traceData, setMessageId } = useFetchMessageTrace();

useEffect(() => {
setMessageId(currentMessageId);
}, [currentMessageId, setMessageId]);

const getNodeName = useCallback(
(nodeId: string) => {
if ('begin' === nodeId) return t('flow.begin');
return getNode(nodeId)?.data.name;
},
[getNode],
);

const startedNodeList = useMemo(() => {
const duplicateList = currentEventListWithoutMessage.filter(
(x) => x.event === MessageEventType.NodeStarted,
) as INodeEvent[];

// Remove duplicate nodes
return duplicateList.reduce<Array<INodeEvent>>((pre, cur) => {
if (pre.every((x) => x.data.component_id !== cur.data.component_id)) {
pre.push(cur);
}
return pre;
}, []);
}, [currentEventListWithoutMessage]);

const hasTrace = useCallback(
(componentId: string) => {
if (Array.isArray(traceData)) {
return traceData?.some((x) => x.component_id === componentId);
}
return false;
},
[traceData],
);

const filterTrace = useCallback(
(componentId: string) => {
const trace = traceData
?.filter((x) => x.component_id === componentId)
.reduce<ITraceData['trace']>((pre, cur) => {
pre.push(...cur.trace);

return pre;
}, []);
return Array.isArray(trace) ? trace : {};
},
[traceData],
);

const filterFinishedNodeList = useCallback(
(componentId: string) => {
const nodeEventList = currentEventListWithoutMessage
.filter(
(x) =>
x.event === MessageEventType.NodeFinished &&
(x.data as INodeData)?.component_id === componentId,
)
.map((x) => x.data);

return nodeEventList;
},
[currentEventListWithoutMessage],
);

return (
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent className="top-20 right-[620px]">
@@ -161,96 +31,12 @@ export function LogSheet({
</SheetTitle>
</SheetHeader>
<section className="max-h-[82vh] overflow-auto mt-6">
<Timeline>
{startedNodeList.map((x, idx) => {
const nodeDataList = filterFinishedNodeList(x.data.component_id);
const finishNodeIds = nodeDataList.map(
(x: INodeData) => x.component_id,
);
const inputs = getInputsOrOutputs(nodeDataList, 'inputs');
const outputs = getInputsOrOutputs(nodeDataList, 'outputs');
const nodeLabel = getNode(x.data.component_id)?.data.label;
return (
<TimelineItem
key={idx}
step={idx}
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8"
>
<TimelineHeader>
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5 top-6 bg-background-checked" />

<TimelineIndicator className="bg-primary/10 group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 items-center justify-center border-none group-data-[orientation=vertical]/timeline:-left-7">
<div className='relative after:content-[""] after:absolute after:inset-0 after:z-10 after:bg-transparent after:transition-all after:duration-300'>
<div className="absolute inset-0 z-10 flex items-center justify-center ">
<div
className={cn('rounded-full w-6 h-6', {
' border-muted-foreground border-2 border-t-transparent animate-spin ':
!finishNodeIds.includes(x.data.component_id),
})}
></div>
</div>
<div className="size-6 flex items-center justify-center">
<OperatorIcon
className="size-5"
name={nodeLabel as Operator}
></OperatorIcon>
</div>
</div>
</TimelineIndicator>
</TimelineHeader>
<TimelineContent className="text-foreground rounded-lg border mb-5">
<section key={idx}>
<Accordion
type="single"
collapsible
className="bg-background-card px-3"
>
<AccordionItem value={idx.toString()}>
<AccordionTrigger>
<div className="flex gap-2 items-center">
<span>{getNodeName(x.data?.component_id)}</span>
<span className="text-text-sub-title text-xs">
{x.data.elapsed_time?.toString().slice(0, 6)}
</span>
<span
className={cn(
'border-background -end-1 -top-1 size-2 rounded-full border-2 bg-dot-green',
{ 'text-dot-green': x.data.error === null },
{ 'text-dot-red': x.data.error !== null },
)}
>
<span className="sr-only">Online</span>
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
<JsonViewer
data={inputs}
title="Input"
></JsonViewer>

{hasTrace(x.data.component_id) && (
<JsonViewer
data={filterTrace(x.data.component_id)}
title={'Trace'}
></JsonViewer>
)}

<JsonViewer
data={outputs}
title={'Output'}
></JsonViewer>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
</TimelineContent>
</TimelineItem>
);
})}
</Timeline>
<WorkFlowTimeline
currentEventListWithoutMessage={currentEventListWithoutMessageById(
currentMessageId,
)}
currentMessageId={currentMessageId}
/>
</section>
</SheetContent>
</Sheet>

+ 118
- 0
web/src/pages/agent/log-sheet/toolTimelineItem.tsx 查看文件

@@ -0,0 +1,118 @@
import {
TimelineContent,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
} from '@/components/originui/timeline';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { cn } from '@/lib/utils';
import { Operator } from '../constant';
import OperatorIcon from '../operator-icon';
import { JsonViewer } from './workFlowTimeline';

const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => {
if (!tools || tools.length === 0 || !Array.isArray(tools)) return null;
const blackList = ['analyze_task', 'add_memory', 'gen_citations'];
const filteredTools = tools.filter(
(tool) => !blackList.includes(tool.tool_name),
);
return (
<>
{filteredTools?.map((tool, idx) => {
return (
<TimelineItem
key={idx}
step={idx}
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8"
>
<TimelineHeader>
<TimelineSeparator
className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5 top-6"
style={{
background:
idx < filteredTools.length - 1
? 'repeating-linear-gradient( to bottom, rgba(76, 164, 231, 1), rgba(76, 164, 231, 1) 5px, transparent 5px, transparent 10px'
: 'rgba(76, 164, 231, 1)',
width: '1px',
}}
/>

<TimelineIndicator
className={cn(
'group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 p-1 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7',
{
'border border-blue-500': !(
idx >= filteredTools.length - 1 && tool.result === '...'
),
},
)}
>
<div className='relative after:content-[""] after:absolute after:inset-0 after:z-10 after:bg-transparent after:transition-all after:duration-300'>
<div className="absolute inset-0 z-10 flex items-center justify-center ">
<div
className={cn('rounded-full w-6 h-6', {
' border-muted-foreground border-2 border-t-transparent animate-spin ':
idx >= filteredTools.length - 1 &&
tool.result === '...',
})}
></div>
</div>
<div className="size-6 flex items-center justify-center">
<OperatorIcon
className="size-4"
name={'Agent' as Operator}
></OperatorIcon>
</div>
</div>
</TimelineIndicator>
</TimelineHeader>
<TimelineContent className="text-foreground rounded-lg border mb-5">
<section key={idx}>
<Accordion
type="single"
collapsible
className="bg-background-card px-3"
>
<AccordionItem value={idx.toString()}>
<AccordionTrigger>
<div className="flex gap-2 items-center">
<span>{tool.tool_name}</span>
<span className="text-text-sub-title text-xs">
{/* 0:00
{x.data.elapsed_time?.toString().slice(0, 6)} */}
</span>
<span
className={cn(
'border-background -end-1 -top-1 size-2 rounded-full border-2 bg-dot-green',
)}
>
<span className="sr-only">Online</span>
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
<JsonViewer
data={tool.result}
title="content"
></JsonViewer>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
</TimelineContent>
</TimelineItem>
);
})}
</>
);
};

export default ToolTimelineItem;

+ 264
- 0
web/src/pages/agent/log-sheet/workFlowTimeline.tsx 查看文件

@@ -0,0 +1,264 @@
import {
Timeline,
TimelineContent,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
} from '@/components/originui/timeline';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import {
INodeData,
INodeEvent,
MessageEventType,
} from '@/hooks/use-send-message';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { get } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import JsonView from 'react18-json-view';
import { Operator } from '../constant';
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
import OperatorIcon from '../operator-icon';
import ToolTimelineItem from './toolTimelineItem';
type LogFlowTimelineProps = Pick<
ReturnType<typeof useCacheChatLog>,
'currentEventListWithoutMessage' | 'currentMessageId'
> & { canvasId?: string };
export function JsonViewer({
data,
title,
}: {
data: Record<string, any>;
title: string;
}) {
return (
<section className="space-y-2">
<div>{title}</div>
<JsonView
src={data}
displaySize
collapseStringsAfterLength={100000000000}
className="w-full h-[200px] break-words overflow-auto p-2 bg-slate-800"
/>
</section>
);
}

function getInputsOrOutputs(
nodeEventList: INodeData[],
field: 'inputs' | 'outputs',
) {
const inputsOrOutputs = nodeEventList.map((x) => get(x, field, {}));

if (inputsOrOutputs.length < 2) {
return inputsOrOutputs[0] || {};
}

return inputsOrOutputs;
}
export const WorkFlowTimeline = ({
currentEventListWithoutMessage,
currentMessageId,
canvasId,
}: LogFlowTimelineProps) => {
// const getNode = useGraphStore((state) => state.getNode);
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);

const { data: traceData, setMessageId } = useFetchMessageTrace(
isStopFetchTrace,
canvasId,
);

useEffect(() => {
setMessageId(currentMessageId);
}, [currentMessageId, setMessageId]);
// const getNodeName = useCallback(
// (nodeId: string) => {
// if ('begin' === nodeId) return t('flow.begin');
// return getNode(nodeId)?.data.name;
// },
// [getNode],
// );
// const getNodeById = useCallback(
// (nodeId: string) => {
// const data = currentEventListWithoutMessage
// .map((x) => x.data)
// .filter((x) => x.component_id === nodeId);
// if ('begin' === nodeId) return t('flow.begin');
// if (data && data.length) {
// return data[0];
// }
// return {};
// },
// [currentEventListWithoutMessage],
// );
const startedNodeList = useMemo(() => {
const finish = currentEventListWithoutMessage?.some(
(item) => item.event === MessageEventType.WorkflowFinished,
);
setISStopFetchTrace(finish);
const duplicateList = currentEventListWithoutMessage?.filter(
(x) => x.event === MessageEventType.NodeStarted,
) as INodeEvent[];

// Remove duplicate nodes
return duplicateList?.reduce<Array<INodeEvent>>((pre, cur) => {
if (pre.every((x) => x.data.component_id !== cur.data.component_id)) {
pre.push(cur);
}
return pre;
}, []);
}, [currentEventListWithoutMessage]);

const hasTrace = useCallback(
(componentId: string) => {
if (Array.isArray(traceData)) {
return traceData?.some((x) => x.component_id === componentId);
}
return false;
},
[traceData],
);

const filterTrace = useCallback(
(componentId: string) => {
const trace = traceData
?.filter((x) => x.component_id === componentId)
.reduce<ITraceData['trace']>((pre, cur) => {
pre.push(...cur.trace);

return pre;
}, []);
return Array.isArray(trace) ? trace : [{}];
},
[traceData],
);

const filterFinishedNodeList = useCallback(
(componentId: string) => {
const nodeEventList = currentEventListWithoutMessage
.filter(
(x) =>
x.event === MessageEventType.NodeFinished &&
(x.data as INodeData)?.component_id === componentId,
)
.map((x) => x.data);

return nodeEventList;
},
[currentEventListWithoutMessage],
);
return (
<Timeline>
{startedNodeList?.map((x, idx) => {
const nodeDataList = filterFinishedNodeList(x.data.component_id);
const finishNodeIds = nodeDataList.map(
(x: INodeData) => x.component_id,
);
const inputs = getInputsOrOutputs(nodeDataList, 'inputs');
const outputs = getInputsOrOutputs(nodeDataList, 'outputs');
const nodeLabel = x.data.component_type;
return (
<>
<TimelineItem
key={idx}
step={idx}
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8"
>
<TimelineHeader>
<TimelineSeparator
className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5 top-6 bg-background-checked"
style={{
background:
x.data.component_type === 'Agent'
? 'repeating-linear-gradient( to bottom, rgba(76, 164, 231, 1), rgba(76, 164, 231, 1) 5px, transparent 5px, transparent 10px'
: '',
}}
/>

<TimelineIndicator
className={cn(
' group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 p-1 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7',
{
'border border-blue-500': finishNodeIds.includes(
x.data.component_id,
),
},
)}
>
<div className='relative after:content-[""] after:absolute after:inset-0 after:z-10 after:bg-transparent after:transition-all after:duration-300'>
<div className="absolute inset-0 z-10 flex items-center justify-center ">
<div
className={cn('rounded-full w-6 h-6', {
' border-muted-foreground border-2 border-t-transparent animate-spin ':
!finishNodeIds.includes(x.data.component_id),
})}
></div>
</div>
<div className="size-6 flex items-center justify-center">
<OperatorIcon
className="size-4"
name={nodeLabel as Operator}
></OperatorIcon>
</div>
</div>
</TimelineIndicator>
</TimelineHeader>
<TimelineContent className="text-foreground rounded-lg border mb-5">
<section key={idx}>
<Accordion
type="single"
collapsible
className="bg-background-card px-3"
>
<AccordionItem value={idx.toString()}>
<AccordionTrigger>
<div className="flex gap-2 items-center">
<span>{x.data?.component_name}</span>
<span className="text-text-sub-title text-xs">
{x.data.elapsed_time?.toString().slice(0, 6)}
</span>
<span
className={cn(
'border-background -end-1 -top-1 size-2 rounded-full border-2 bg-dot-green',
{ 'text-dot-green': x.data.error === null },
{ 'text-dot-red': x.data.error !== null },
)}
>
<span className="sr-only">Online</span>
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
<JsonViewer data={inputs} title="Input"></JsonViewer>

<JsonViewer
data={outputs}
title={'Output'}
></JsonViewer>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
</TimelineContent>
</TimelineItem>
{hasTrace(x.data.component_id) && (
<ToolTimelineItem
tools={filterTrace(x.data.component_id)}
></ToolTimelineItem>
)}
</>
);
})}
</Timeline>
);
};

+ 6
- 3
web/src/pages/next-chats/hooks/use-send-shared-message.ts 查看文件

@@ -1,4 +1,5 @@
import { SharedFrom } from '@/constants/chat';
import { IEventList } from '@/hooks/use-send-message';
import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message';
import trim from 'lodash/trim';
import { useSearchParams } from 'umi';
@@ -27,14 +28,16 @@ export const useGetSharedChatSearchParams = () => {
};
};

export function useSendNextSharedMessage() {
export const useSendNextSharedMessage = (
addEventList: (data: IEventList, messageId: string) => void,
) => {
const { from, sharedId: conversationId } = useGetSharedChatSearchParams();
const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`;

const ret = useSendAgentMessage(url);
const ret = useSendAgentMessage(url, addEventList);

return {
...ret,
hasError: false,
};
}
};

+ 27
- 3
web/src/pages/next-chats/share/index.tsx 查看文件

@@ -11,6 +11,7 @@ import {
} from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import i18n from '@/locales/config';
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import React, { forwardRef, useCallback, useMemo } from 'react';
@@ -31,7 +32,13 @@ const ChatContainer = () => {

const { uploadCanvasFile, loading } =
useUploadCanvasFileWithProgress(conversationId);

const {
addEventList,
setCurrentMessageId,
currentEventListWithoutMessageById,
clearEventList,
currentMessageId,
} = useCacheChatLog();
const {
handlePressEnter,
handleInputChange,
@@ -43,9 +50,22 @@ const ChatContainer = () => {
stopOutputMessage,
findReferenceByMessageId,
appendUploadResponseList,
} = useSendNextSharedMessage();
} = useSendNextSharedMessage(addEventList);

const sendDisabled = useSendButtonDisabled(value);

// useEffect(() => {
// if (derivedMessages.length) {
// const derivedMessagesFilter = derivedMessages.filter(
// (message) => message.role === MessageType.Assistant,
// );
// if (derivedMessagesFilter.length) {
// const message = derivedMessagesFilter[derivedMessagesFilter.length - 1];
// setCurrentMessageId(message.id);
// }
// }
// }, [derivedMessages, setCurrentMessageId]);

const useFetchAvatar = useMemo(() => {
return from === SharedFrom.Agent
? useFetchAgentAvatar
@@ -81,6 +101,11 @@ const ChatContainer = () => {
return (
<MessageItem
visibleAvatar={visibleAvatar}
conversationId={conversationId}
currentEventListWithoutMessageById={
currentEventListWithoutMessageById
}
setCurrentMessageId={setCurrentMessageId}
key={buildMessageUuidWithRole(message)}
avatarDialog={avatarData.avatar}
item={message}
@@ -103,7 +128,6 @@ const ChatContainer = () => {
</div>
<div ref={ref} />
</div>

<NextMessageInput
isShared
value={value}

+ 5
- 0
web/src/services/agent-service.ts 查看文件

@@ -1,5 +1,6 @@
import api from '@/utils/api';
import { registerNextServer } from '@/utils/register-server';
import request from '@/utils/request';

const {
getCanvasSSE,
@@ -104,4 +105,8 @@ const methods = {

const agentService = registerNextServer<keyof typeof methods>(methods);

export const fetchTrace = (data: { canvas_id: string; message_id: string }) => {
return request.get(methods.trace.url, { params: data });
};

export default agentService;

Loading…
取消
儲存