Feat: Add note node #3221 (#8728)

### What problem does this PR solve?

Feat: Add note node #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-07-08 19:18:55 +08:00
committed by GitHub
parent 2a03d49a84
commit 3fe143d84a
17 changed files with 414 additions and 407 deletions

View File

@ -19,11 +19,15 @@ import {
SheetTitle,
} from '@/components/ui/sheet';
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import { ILogEvent, MessageEventType } from '@/hooks/use-send-message';
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 { isEmpty } from 'lodash';
import { get } from 'lodash';
import { BellElectric, NotebookText } from 'lucide-react';
import { useCallback, useEffect, useMemo } from 'react';
import JsonView from 'react18-json-view';
@ -57,25 +61,19 @@ function JsonViewer({
);
}
function concatData(
firstRecord: Record<string, any> | Array<Record<string, any>>,
nextRecord: Record<string, any> | Array<Record<string, any>>,
function getInputsOrOutputs(
nodeEventList: INodeData[],
field: 'inputs' | 'outputs',
) {
let result: Array<Record<string, any>> = [];
const inputsOrOutputs = nodeEventList.map((x) => get(x, field, {}));
if (!isEmpty(firstRecord)) {
result = result.concat(firstRecord);
if (inputsOrOutputs.length < 2) {
return inputsOrOutputs[0] || {};
}
if (!isEmpty(nextRecord)) {
result = result.concat(nextRecord);
}
return isEmpty(result) ? {} : result;
return inputsOrOutputs;
}
type EventWithIndex = { startNodeIdx: number } & ILogEvent;
export function LogSheet({
hideModal,
currentEventListWithoutMessage,
@ -96,68 +94,58 @@ export function LogSheet({
[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) => {
return traceData
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],
);
// Look up to find the nearest start component id and concatenate the finish and log data into one
const finishedNodeList = useMemo(() => {
return currentEventListWithoutMessage.filter(
(x) =>
x.event === MessageEventType.NodeFinished ||
x.event === MessageEventType.NodeLogs,
) as ILogEvent[];
}, [currentEventListWithoutMessage]);
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);
const nextList = useMemo(() => {
return finishedNodeList.reduce<Array<EventWithIndex>>((pre, cur) => {
const startNodeIdx = (
currentEventListWithoutMessage as Array<ILogEvent>
).findLastIndex(
(x) =>
x.data.component_id === cur.data.component_id &&
x.event === MessageEventType.NodeStarted,
);
const item = pre.find((x) => x.startNodeIdx === startNodeIdx);
const { inputs = {}, outputs = {} } = cur.data;
if (item) {
const { inputs: inputList, outputs: outputList } = item.data;
item.data = {
...item.data,
inputs: concatData(inputList, inputs),
outputs: concatData(outputList, outputs),
};
} else {
pre.push({
...cur,
startNodeIdx,
});
}
return pre;
}, []);
}, [currentEventListWithoutMessage, finishedNodeList]);
return nodeEventList;
},
[currentEventListWithoutMessage],
);
return (
<Sheet open onOpenChange={hideModal} modal={false}>
@ -170,76 +158,75 @@ export function LogSheet({
</SheetHeader>
<section className="max-h-[82vh] overflow-auto mt-6">
<Timeline>
{nextList.map((x, idx) => (
<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" />
{startedNodeList.map((x, idx) => {
const nodeDataList = filterFinishedNodeList(x.data.component_id);
const inputs = getInputsOrOutputs(nodeDataList, 'inputs');
const outputs = getInputsOrOutputs(nodeDataList, 'outputs');
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">
<BellElectric className="size-5" />
{/* <img
src={item.image}
alt={item.title}
className="size-6 rounded-full"
/> */}
</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={x.data.inputs}
title="Input"
></JsonViewer>
{hasTrace(x.data.component_id) && (
<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">
<BellElectric className="size-5" />
</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={filterTrace(x.data.component_id) ?? {}}
title={'Trace'}
data={inputs}
title="Input"
></JsonViewer>
)}
<JsonViewer
data={x.data.outputs}
title={'Output'}
></JsonViewer>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
{/* <TimelineDate className="mt-1 mb-0">{item.date}</TimelineDate> */}
</TimelineContent>
</TimelineItem>
))}
{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>
</section>
</SheetContent>