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)
This commit is contained in:
chanx
2025-07-28 16:48:46 +08:00
committed by GitHub
parent cc0227cf6e
commit 381f9df941
12 changed files with 520 additions and 256 deletions

View File

@ -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>