mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-26 07:28:44 -05:00
Compare commits
6 Commits
dev
...
abhi/show-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb852a947b | ||
|
|
10cc347563 | ||
|
|
71c0f909f3 | ||
|
|
9d9fea700b | ||
|
|
4e25b1d0b2 | ||
|
|
2cd9ec5106 |
@@ -38,8 +38,12 @@ export const AgentOutputs = ({ flowID }: { flowID: string | null }) => {
|
|||||||
|
|
||||||
return outputNodes
|
return outputNodes
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
const executionResult = node.data.nodeExecutionResult;
|
const executionResults = node.data.nodeExecutionResults || [];
|
||||||
const outputData = executionResult?.output_data?.output;
|
const latestResult =
|
||||||
|
executionResults.length > 0
|
||||||
|
? executionResults[executionResults.length - 1]
|
||||||
|
: undefined;
|
||||||
|
const outputData = latestResult?.output_data?.output;
|
||||||
|
|
||||||
const renderer = globalRegistry.getRenderer(outputData);
|
const renderer = globalRegistry.getRenderer(outputData);
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ export const useRunInputDialog = ({
|
|||||||
Object.entries(credentialValues).filter(([_, cred]) => cred && cred.id),
|
Object.entries(credentialValues).filter(([_, cred]) => cred && cred.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useNodeStore.getState().clearAllNodeExecutionResults();
|
||||||
|
useNodeStore.getState().cleanNodesStatuses();
|
||||||
|
|
||||||
await executeGraph({
|
await executeGraph({
|
||||||
graphId: flowID ?? "",
|
graphId: flowID ?? "",
|
||||||
graphVersion: flowVersion || null,
|
graphVersion: flowVersion || null,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export type CustomNodeData = {
|
|||||||
uiType: BlockUIType;
|
uiType: BlockUIType;
|
||||||
block_id: string;
|
block_id: string;
|
||||||
status?: AgentExecutionStatus;
|
status?: AgentExecutionStatus;
|
||||||
nodeExecutionResult?: NodeExecutionResult;
|
nodeExecutionResults?: NodeExecutionResult[];
|
||||||
staticOutput?: boolean;
|
staticOutput?: boolean;
|
||||||
// TODO : We need better type safety for the following backend fields.
|
// TODO : We need better type safety for the following backend fields.
|
||||||
costs: BlockCost[];
|
costs: BlockCost[];
|
||||||
@@ -75,7 +75,11 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
|||||||
(value) => value !== null && value !== undefined && value !== "",
|
(value) => value !== null && value !== undefined && value !== "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const outputData = data.nodeExecutionResult?.output_data;
|
const latestResult =
|
||||||
|
data.nodeExecutionResults && data.nodeExecutionResults.length > 0
|
||||||
|
? data.nodeExecutionResults[data.nodeExecutionResults.length - 1]
|
||||||
|
: undefined;
|
||||||
|
const outputData = latestResult?.output_data;
|
||||||
const hasOutputError =
|
const hasOutputError =
|
||||||
typeof outputData === "object" &&
|
typeof outputData === "object" &&
|
||||||
outputData !== null &&
|
outputData !== null &&
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ import { useNodeOutput } from "./useNodeOutput";
|
|||||||
import { ViewMoreData } from "./components/ViewMoreData";
|
import { ViewMoreData } from "./components/ViewMoreData";
|
||||||
|
|
||||||
export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
||||||
const { outputData, copiedKey, handleCopy, executionResultId, inputData } =
|
const {
|
||||||
useNodeOutput(nodeId);
|
latestOutputData,
|
||||||
|
copiedKey,
|
||||||
|
handleCopy,
|
||||||
|
executionResultId,
|
||||||
|
latestInputData,
|
||||||
|
} = useNodeOutput(nodeId);
|
||||||
|
|
||||||
if (Object.keys(outputData).length === 0) {
|
if (Object.keys(latestOutputData).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,18 +46,19 @@ export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Text variant="small-medium">Input</Text>
|
<Text variant="small-medium">Input</Text>
|
||||||
|
|
||||||
<ContentRenderer value={inputData} shortContent={false} />
|
<ContentRenderer value={latestInputData} shortContent={false} />
|
||||||
|
|
||||||
<div className="mt-1 flex justify-end gap-1">
|
<div className="mt-1 flex justify-end gap-1">
|
||||||
<NodeDataViewer
|
<NodeDataViewer
|
||||||
data={inputData}
|
|
||||||
pinName="Input"
|
pinName="Input"
|
||||||
|
nodeId={nodeId}
|
||||||
execId={executionResultId}
|
execId={executionResultId}
|
||||||
|
dataType="input"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleCopy("input", inputData)}
|
onClick={() => handleCopy("input", latestInputData)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
||||||
copiedKey === "input" &&
|
copiedKey === "input" &&
|
||||||
@@ -68,70 +74,72 @@ export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Object.entries(outputData)
|
{Object.entries(latestOutputData)
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map(([key, value]) => (
|
.map(([key, value]) => {
|
||||||
<div key={key} className="flex flex-col gap-2">
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<Text
|
<div className="flex items-center gap-2">
|
||||||
variant="small-medium"
|
<Text
|
||||||
className="!font-semibold text-slate-600"
|
variant="small-medium"
|
||||||
>
|
className="!font-semibold text-slate-600"
|
||||||
Pin:
|
>
|
||||||
</Text>
|
Pin:
|
||||||
<Text variant="small" className="text-slate-700">
|
</Text>
|
||||||
{beautifyString(key)}
|
<Text variant="small" className="text-slate-700">
|
||||||
</Text>
|
{beautifyString(key)}
|
||||||
</div>
|
</Text>
|
||||||
<div className="w-full space-y-2">
|
</div>
|
||||||
<Text
|
<div className="w-full space-y-2">
|
||||||
variant="small"
|
<Text
|
||||||
className="!font-semibold text-slate-600"
|
variant="small"
|
||||||
>
|
className="!font-semibold text-slate-600"
|
||||||
Data:
|
>
|
||||||
</Text>
|
Data:
|
||||||
<div className="relative space-y-2">
|
</Text>
|
||||||
{value.map((item, index) => (
|
<div className="relative space-y-2">
|
||||||
<div key={index}>
|
{value.map((item, index) => (
|
||||||
<ContentRenderer value={item} shortContent={true} />
|
<div key={index}>
|
||||||
</div>
|
<ContentRenderer
|
||||||
))}
|
value={item}
|
||||||
|
shortContent={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<div className="mt-1 flex justify-end gap-1">
|
<div className="mt-1 flex justify-end gap-1">
|
||||||
<NodeDataViewer
|
<NodeDataViewer
|
||||||
data={value}
|
pinName={key}
|
||||||
pinName={key}
|
nodeId={nodeId}
|
||||||
execId={executionResultId}
|
execId={executionResultId}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleCopy(key, value)}
|
onClick={() => handleCopy(key, value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
||||||
copiedKey === key &&
|
copiedKey === key &&
|
||||||
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{copiedKey === key ? (
|
{copiedKey === key ? (
|
||||||
<CheckIcon size={12} className="text-green-600" />
|
<CheckIcon
|
||||||
) : (
|
size={12}
|
||||||
<CopyIcon size={12} />
|
className="text-green-600"
|
||||||
)}
|
/>
|
||||||
</Button>
|
) : (
|
||||||
|
<CopyIcon size={12} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<ViewMoreData nodeId={nodeId} />
|
||||||
{Object.keys(outputData).length > 2 && (
|
|
||||||
<ViewMoreData
|
|
||||||
outputData={outputData}
|
|
||||||
execId={executionResultId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -19,22 +19,51 @@ import {
|
|||||||
CopyIcon,
|
CopyIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useNodeDataViewer } from "./useNodeDataViewer";
|
import { useNodeDataViewer } from "./useNodeDataViewer";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { NodeDataType } from "../../helpers";
|
||||||
|
|
||||||
interface NodeDataViewerProps {
|
export interface NodeDataViewerProps {
|
||||||
data: any;
|
data?: any;
|
||||||
pinName: string;
|
pinName: string;
|
||||||
|
nodeId?: string;
|
||||||
execId?: string;
|
execId?: string;
|
||||||
isViewMoreData?: boolean;
|
isViewMoreData?: boolean;
|
||||||
|
dataType?: NodeDataType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeDataViewer: FC<NodeDataViewerProps> = ({
|
export const NodeDataViewer: FC<NodeDataViewerProps> = ({
|
||||||
data,
|
data,
|
||||||
pinName,
|
pinName,
|
||||||
|
nodeId,
|
||||||
execId = "N/A",
|
execId = "N/A",
|
||||||
isViewMoreData = false,
|
isViewMoreData = false,
|
||||||
|
dataType = "output",
|
||||||
}) => {
|
}) => {
|
||||||
|
const executionResults = useNodeStore(
|
||||||
|
useShallow((state) =>
|
||||||
|
nodeId ? state.getNodeExecutionResults(nodeId) : [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const latestInputData = useNodeStore(
|
||||||
|
useShallow((state) =>
|
||||||
|
nodeId ? state.getLatestNodeInputData(nodeId) : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const accumulatedOutputData = useNodeStore(
|
||||||
|
useShallow((state) =>
|
||||||
|
nodeId ? state.getAccumulatedNodeOutputData(nodeId) : {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedData =
|
||||||
|
data ??
|
||||||
|
(dataType === "input"
|
||||||
|
? (latestInputData ?? {})
|
||||||
|
: (accumulatedOutputData[pinName] ?? []));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
outputItems,
|
outputItems,
|
||||||
copyExecutionId,
|
copyExecutionId,
|
||||||
@@ -42,7 +71,20 @@ export const NodeDataViewer: FC<NodeDataViewerProps> = ({
|
|||||||
handleDownloadItem,
|
handleDownloadItem,
|
||||||
dataArray,
|
dataArray,
|
||||||
copiedIndex,
|
copiedIndex,
|
||||||
} = useNodeDataViewer(data, pinName, execId);
|
groupedExecutions,
|
||||||
|
totalGroupedItems,
|
||||||
|
handleCopyGroupedItem,
|
||||||
|
handleDownloadGroupedItem,
|
||||||
|
copiedKey,
|
||||||
|
} = useNodeDataViewer(
|
||||||
|
resolvedData,
|
||||||
|
pinName,
|
||||||
|
execId,
|
||||||
|
executionResults,
|
||||||
|
dataType,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldGroupExecutions = groupedExecutions.length > 0;
|
||||||
return (
|
return (
|
||||||
<Dialog styling={{ width: "600px" }}>
|
<Dialog styling={{ width: "600px" }}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -68,44 +110,141 @@ export const NodeDataViewer: FC<NodeDataViewerProps> = ({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Text variant="large-medium" className="text-slate-900">
|
<Text variant="large-medium" className="text-slate-900">
|
||||||
Full Output Preview
|
Full {dataType === "input" ? "Input" : "Output"} Preview
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-full border border-slate-300 bg-slate-100 px-3 py-1.5 text-xs font-medium text-black">
|
<div className="rounded-full border border-slate-300 bg-slate-100 px-3 py-1.5 text-xs font-medium text-black">
|
||||||
{dataArray.length} item{dataArray.length !== 1 ? "s" : ""} total
|
{shouldGroupExecutions ? totalGroupedItems : dataArray.length}{" "}
|
||||||
|
item
|
||||||
|
{shouldGroupExecutions
|
||||||
|
? totalGroupedItems !== 1
|
||||||
|
? "s"
|
||||||
|
: ""
|
||||||
|
: dataArray.length !== 1
|
||||||
|
? "s"
|
||||||
|
: ""}{" "}
|
||||||
|
total
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
<div className="flex items-center gap-2">
|
{shouldGroupExecutions ? (
|
||||||
<Text variant="body" className="text-slate-600">
|
<div>
|
||||||
Execution ID:
|
Pin:{" "}
|
||||||
</Text>
|
<span className="font-semibold">{beautifyString(pinName)}</span>
|
||||||
<Text
|
</div>
|
||||||
variant="body-medium"
|
) : (
|
||||||
className="rounded-full border border-gray-300 bg-gray-50 px-2 py-1 font-mono text-xs"
|
<>
|
||||||
>
|
<div className="flex items-center gap-2">
|
||||||
{execId}
|
<Text variant="body" className="text-slate-600">
|
||||||
</Text>
|
Execution ID:
|
||||||
<Button
|
</Text>
|
||||||
variant="ghost"
|
<Text
|
||||||
size="small"
|
variant="body-medium"
|
||||||
onClick={copyExecutionId}
|
className="rounded-full border border-gray-300 bg-gray-50 px-2 py-1 font-mono text-xs"
|
||||||
className="h-6 w-6 min-w-0 p-0"
|
>
|
||||||
>
|
{execId}
|
||||||
<CopyIcon size={14} />
|
</Text>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
variant="ghost"
|
||||||
<div className="mt-2">
|
size="small"
|
||||||
Pin:{" "}
|
onClick={copyExecutionId}
|
||||||
<span className="font-semibold">{beautifyString(pinName)}</span>
|
className="h-6 w-6 min-w-0 p-0"
|
||||||
</div>
|
>
|
||||||
|
<CopyIcon size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
Pin:{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{beautifyString(pinName)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
{dataArray.length > 0 ? (
|
{shouldGroupExecutions ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{groupedExecutions.map((execution) => (
|
||||||
|
<div
|
||||||
|
key={execution.execId}
|
||||||
|
className="rounded-3xl border border-slate-200 bg-white p-4 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Text variant="body" className="text-slate-600">
|
||||||
|
Execution ID:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="body-medium"
|
||||||
|
className="rounded-full border border-gray-300 bg-gray-50 px-2 py-1 font-mono text-xs"
|
||||||
|
>
|
||||||
|
{execution.execId}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 space-y-4">
|
||||||
|
{execution.outputItems.length > 0 ? (
|
||||||
|
execution.outputItems.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.key}
|
||||||
|
className="group flex items-start gap-4"
|
||||||
|
>
|
||||||
|
<div className="w-full flex-1">
|
||||||
|
<OutputItem
|
||||||
|
value={item.value}
|
||||||
|
metadata={item.metadata}
|
||||||
|
renderer={item.renderer}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-fit gap-3">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="min-w-0 p-1"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
handleCopyGroupedItem(
|
||||||
|
execution.execId,
|
||||||
|
index,
|
||||||
|
item,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
aria-label="Copy item"
|
||||||
|
>
|
||||||
|
{copiedKey ===
|
||||||
|
`${execution.execId}-${index}` ? (
|
||||||
|
<CheckIcon className="size-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="size-4 text-black" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="min-w-0 p-1"
|
||||||
|
onClick={() =>
|
||||||
|
handleDownloadGroupedItem(item)
|
||||||
|
}
|
||||||
|
aria-label="Download item"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="size-4 text-black" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="py-4 text-center text-gray-500">
|
||||||
|
No data available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : dataArray.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{outputItems.map((item, index) => (
|
{outputItems.map((item, index) => (
|
||||||
<div key={item.key} className="group relative">
|
<div key={item.key} className="group relative">
|
||||||
|
|||||||
@@ -1,82 +1,70 @@
|
|||||||
import type { OutputMetadata } from "@/components/contextual/OutputRenderers";
|
|
||||||
import { globalRegistry } from "@/components/contextual/OutputRenderers";
|
|
||||||
import { downloadOutputs } from "@/components/contextual/OutputRenderers/utils/download";
|
import { downloadOutputs } from "@/components/contextual/OutputRenderers/utils/download";
|
||||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||||
import { beautifyString } from "@/lib/utils";
|
import { beautifyString } from "@/lib/utils";
|
||||||
import React, { useMemo, useState } from "react";
|
import { useState } from "react";
|
||||||
|
import type { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||||
|
import {
|
||||||
|
NodeDataType,
|
||||||
|
createOutputItems,
|
||||||
|
getExecutionData,
|
||||||
|
normalizeToArray,
|
||||||
|
type OutputItem,
|
||||||
|
} from "../../helpers";
|
||||||
|
|
||||||
|
export type GroupedExecution = {
|
||||||
|
execId: string;
|
||||||
|
outputItems: Array<OutputItem>;
|
||||||
|
};
|
||||||
|
|
||||||
export const useNodeDataViewer = (
|
export const useNodeDataViewer = (
|
||||||
data: any,
|
data: any,
|
||||||
pinName: string,
|
pinName: string,
|
||||||
execId: string,
|
execId: string,
|
||||||
|
executionResults?: NodeExecutionResult[],
|
||||||
|
dataType?: NodeDataType,
|
||||||
) => {
|
) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||||
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
|
|
||||||
// Normalize data to array format
|
const dataArray = Array.isArray(data) ? data : [data];
|
||||||
const dataArray = useMemo(() => {
|
|
||||||
return Array.isArray(data) ? data : [data];
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
// Prepare items for the enhanced renderer system
|
const outputItems =
|
||||||
const outputItems = useMemo(() => {
|
!dataArray || dataArray.length === 0
|
||||||
if (!dataArray) return [];
|
? []
|
||||||
|
: createOutputItems(dataArray).map((item, index) => ({
|
||||||
const items: Array<{
|
...item,
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
value: unknown;
|
|
||||||
metadata?: OutputMetadata;
|
|
||||||
renderer: any;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
dataArray.forEach((value, index) => {
|
|
||||||
const metadata: OutputMetadata = {};
|
|
||||||
|
|
||||||
// Extract metadata from the value if it's an object
|
|
||||||
if (
|
|
||||||
typeof value === "object" &&
|
|
||||||
value !== null &&
|
|
||||||
!React.isValidElement(value)
|
|
||||||
) {
|
|
||||||
const objValue = value as any;
|
|
||||||
if (objValue.type) metadata.type = objValue.type;
|
|
||||||
if (objValue.mimeType) metadata.mimeType = objValue.mimeType;
|
|
||||||
if (objValue.filename) metadata.filename = objValue.filename;
|
|
||||||
if (objValue.language) metadata.language = objValue.language;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = globalRegistry.getRenderer(value, metadata);
|
|
||||||
if (renderer) {
|
|
||||||
items.push({
|
|
||||||
key: `item-${index}`,
|
|
||||||
label: index === 0 ? beautifyString(pinName) : "",
|
label: index === 0 ? beautifyString(pinName) : "",
|
||||||
value,
|
}));
|
||||||
metadata,
|
|
||||||
renderer,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback to text renderer
|
|
||||||
const textRenderer = globalRegistry
|
|
||||||
.getAllRenderers()
|
|
||||||
.find((r) => r.name === "TextRenderer");
|
|
||||||
if (textRenderer) {
|
|
||||||
items.push({
|
|
||||||
key: `item-${index}`,
|
|
||||||
label: index === 0 ? beautifyString(pinName) : "",
|
|
||||||
value:
|
|
||||||
typeof value === "string"
|
|
||||||
? value
|
|
||||||
: JSON.stringify(value, null, 2),
|
|
||||||
metadata,
|
|
||||||
renderer: textRenderer,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
const groupedExecutions =
|
||||||
}, [dataArray, pinName]);
|
!executionResults || executionResults.length === 0
|
||||||
|
? []
|
||||||
|
: [...executionResults].reverse().map((result) => {
|
||||||
|
const rawData = getExecutionData(
|
||||||
|
result,
|
||||||
|
dataType || "output",
|
||||||
|
pinName,
|
||||||
|
);
|
||||||
|
let dataArray: unknown[];
|
||||||
|
if (dataType === "input") {
|
||||||
|
dataArray =
|
||||||
|
rawData !== undefined && rawData !== null ? [rawData] : [];
|
||||||
|
} else {
|
||||||
|
dataArray = normalizeToArray(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputItems = createOutputItems(dataArray);
|
||||||
|
return {
|
||||||
|
execId: result.node_exec_id,
|
||||||
|
outputItems,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalGroupedItems = groupedExecutions.reduce(
|
||||||
|
(total, execution) => total + execution.outputItems.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
const copyExecutionId = () => {
|
const copyExecutionId = () => {
|
||||||
navigator.clipboard.writeText(execId).then(() => {
|
navigator.clipboard.writeText(execId).then(() => {
|
||||||
@@ -122,6 +110,45 @@ export const useNodeDataViewer = (
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopyGroupedItem = async (
|
||||||
|
execId: string,
|
||||||
|
index: number,
|
||||||
|
item: OutputItem,
|
||||||
|
) => {
|
||||||
|
const copyContent = item.renderer.getCopyContent(item.value, item.metadata);
|
||||||
|
|
||||||
|
if (!copyContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let text: string;
|
||||||
|
if (typeof copyContent.data === "string") {
|
||||||
|
text = copyContent.data;
|
||||||
|
} else if (copyContent.fallbackText) {
|
||||||
|
text = copyContent.fallbackText;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopiedKey(`${execId}-${index}`);
|
||||||
|
setTimeout(() => setCopiedKey(null), 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadGroupedItem = (item: OutputItem) => {
|
||||||
|
downloadOutputs([
|
||||||
|
{
|
||||||
|
value: item.value,
|
||||||
|
metadata: item.metadata,
|
||||||
|
renderer: item.renderer,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outputItems,
|
outputItems,
|
||||||
dataArray,
|
dataArray,
|
||||||
@@ -129,5 +156,10 @@ export const useNodeDataViewer = (
|
|||||||
handleCopyItem,
|
handleCopyItem,
|
||||||
handleDownloadItem,
|
handleDownloadItem,
|
||||||
copiedIndex,
|
copiedIndex,
|
||||||
|
groupedExecutions,
|
||||||
|
totalGroupedItems,
|
||||||
|
handleCopyGroupedItem,
|
||||||
|
handleDownloadGroupedItem,
|
||||||
|
copiedKey,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,16 +8,28 @@ import { useState } from "react";
|
|||||||
import { NodeDataViewer } from "./NodeDataViewer/NodeDataViewer";
|
import { NodeDataViewer } from "./NodeDataViewer/NodeDataViewer";
|
||||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||||
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
|
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
|
||||||
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import {
|
||||||
|
NodeDataType,
|
||||||
|
getExecutionEntries,
|
||||||
|
normalizeToArray,
|
||||||
|
} from "../helpers";
|
||||||
|
|
||||||
export const ViewMoreData = ({
|
export const ViewMoreData = ({
|
||||||
outputData,
|
nodeId,
|
||||||
execId,
|
dataType = "output",
|
||||||
}: {
|
}: {
|
||||||
outputData: Record<string, Array<any>>;
|
nodeId: string;
|
||||||
execId?: string;
|
dataType?: NodeDataType;
|
||||||
}) => {
|
}) => {
|
||||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const executionResults = useNodeStore(
|
||||||
|
useShallow((state) => state.getNodeExecutionResults(nodeId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reversedExecutionResults = [...executionResults].reverse();
|
||||||
|
|
||||||
const handleCopy = (key: string, value: any) => {
|
const handleCopy = (key: string, value: any) => {
|
||||||
const textToCopy =
|
const textToCopy =
|
||||||
@@ -29,8 +41,8 @@ export const ViewMoreData = ({
|
|||||||
setTimeout(() => setCopiedKey(null), 2000);
|
setTimeout(() => setCopiedKey(null), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyExecutionId = () => {
|
const copyExecutionId = (executionId: string) => {
|
||||||
navigator.clipboard.writeText(execId || "N/A").then(() => {
|
navigator.clipboard.writeText(executionId || "N/A").then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: "Execution ID copied to clipboard!",
|
title: "Execution ID copied to clipboard!",
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
@@ -42,7 +54,7 @@ export const ViewMoreData = ({
|
|||||||
<Dialog styling={{ width: "600px", paddingRight: "16px" }}>
|
<Dialog styling={{ width: "600px", paddingRight: "16px" }}>
|
||||||
<Dialog.Trigger>
|
<Dialog.Trigger>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
className="h-fit w-fit min-w-0 !text-xs"
|
className="h-fit w-fit min-w-0 !text-xs"
|
||||||
>
|
>
|
||||||
@@ -52,83 +64,114 @@ export const ViewMoreData = ({
|
|||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Text variant="h4" className="text-slate-900">
|
<Text variant="h4" className="text-slate-900">
|
||||||
Complete Output Data
|
Complete {dataType === "input" ? "Input" : "Output"} Data
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Text variant="body" className="text-slate-600">
|
|
||||||
Execution ID:
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
variant="body-medium"
|
|
||||||
className="rounded-full border border-gray-300 bg-gray-50 px-2 py-1 font-mono text-xs"
|
|
||||||
>
|
|
||||||
{execId}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="small"
|
|
||||||
onClick={copyExecutionId}
|
|
||||||
className="h-6 w-6 min-w-0 p-0"
|
|
||||||
>
|
|
||||||
<CopyIcon size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{Object.entries(outputData).map(([key, value]) => (
|
{reversedExecutionResults.map((result) => (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div
|
||||||
|
key={result.node_exec_id}
|
||||||
|
className="rounded-3xl border border-slate-200 bg-white p-4 shadow-sm"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Text variant="body" className="text-slate-600">
|
||||||
|
Execution ID:
|
||||||
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
variant="body-medium"
|
variant="body-medium"
|
||||||
className="!font-semibold text-slate-600"
|
className="rounded-full border border-gray-300 bg-gray-50 px-2 py-1 font-mono text-xs"
|
||||||
>
|
>
|
||||||
Pin:
|
{result.node_exec_id}
|
||||||
</Text>
|
|
||||||
<Text variant="body-medium" className="text-slate-700">
|
|
||||||
{beautifyString(key)}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
onClick={() => copyExecutionId(result.node_exec_id)}
|
||||||
|
className="h-6 w-6 min-w-0 p-0"
|
||||||
|
>
|
||||||
|
<CopyIcon size={14} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-2">
|
|
||||||
<Text
|
|
||||||
variant="body-medium"
|
|
||||||
className="!font-semibold text-slate-600"
|
|
||||||
>
|
|
||||||
Data:
|
|
||||||
</Text>
|
|
||||||
<div className="relative space-y-2">
|
|
||||||
{value.map((item, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<ContentRenderer value={item} shortContent={false} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="mt-1 flex justify-end gap-1">
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
<NodeDataViewer
|
{getExecutionEntries(result, dataType).map(
|
||||||
data={value}
|
([key, value]) => {
|
||||||
pinName={key}
|
const normalizedValue = normalizeToArray(value);
|
||||||
execId={execId}
|
return (
|
||||||
isViewMoreData={true}
|
<div key={key} className="flex flex-col gap-2">
|
||||||
/>
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Text
|
||||||
variant="secondary"
|
variant="body-medium"
|
||||||
size="small"
|
className="!font-semibold text-slate-600"
|
||||||
onClick={() => handleCopy(key, value)}
|
>
|
||||||
className={cn(
|
Pin:
|
||||||
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
</Text>
|
||||||
copiedKey === key &&
|
<Text
|
||||||
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
variant="body-medium"
|
||||||
)}
|
className="text-slate-700"
|
||||||
>
|
>
|
||||||
{copiedKey === key ? (
|
{beautifyString(key)}
|
||||||
<CheckIcon size={16} className="text-green-600" />
|
</Text>
|
||||||
) : (
|
</div>
|
||||||
<CopyIcon size={16} />
|
<div className="w-full space-y-2">
|
||||||
)}
|
<Text
|
||||||
</Button>
|
variant="body-medium"
|
||||||
</div>
|
className="!font-semibold text-slate-600"
|
||||||
</div>
|
>
|
||||||
|
Data:
|
||||||
|
</Text>
|
||||||
|
<div className="relative space-y-2">
|
||||||
|
{normalizedValue.map((item, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<ContentRenderer
|
||||||
|
value={item}
|
||||||
|
shortContent={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="mt-1 flex justify-end gap-1">
|
||||||
|
<NodeDataViewer
|
||||||
|
data={normalizedValue}
|
||||||
|
pinName={key}
|
||||||
|
execId={result.node_exec_id}
|
||||||
|
isViewMoreData={true}
|
||||||
|
dataType={dataType}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={() =>
|
||||||
|
handleCopy(
|
||||||
|
`${result.node_exec_id}-${key}`,
|
||||||
|
normalizedValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
||||||
|
copiedKey ===
|
||||||
|
`${result.node_exec_id}-${key}` &&
|
||||||
|
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copiedKey ===
|
||||||
|
`${result.node_exec_id}-${key}` ? (
|
||||||
|
<CheckIcon
|
||||||
|
size={16}
|
||||||
|
className="text-green-600"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CopyIcon size={16} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||||
|
import type { OutputMetadata } from "@/components/contextual/OutputRenderers";
|
||||||
|
import { globalRegistry } from "@/components/contextual/OutputRenderers";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type NodeDataType = "input" | "output";
|
||||||
|
|
||||||
|
export type OutputItem = {
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
metadata?: OutputMetadata;
|
||||||
|
renderer: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeToArray = (value: unknown) => {
|
||||||
|
if (value === undefined) return [];
|
||||||
|
return Array.isArray(value) ? value : [value];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExecutionData = (
|
||||||
|
result: NodeExecutionResult,
|
||||||
|
dataType: NodeDataType,
|
||||||
|
pinName: string,
|
||||||
|
) => {
|
||||||
|
if (dataType === "input") {
|
||||||
|
return result.input_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.output_data?.[pinName];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createOutputItems = (dataArray: unknown[]): Array<OutputItem> => {
|
||||||
|
const items: Array<OutputItem> = [];
|
||||||
|
|
||||||
|
dataArray.forEach((value, index) => {
|
||||||
|
const metadata: OutputMetadata = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
!React.isValidElement(value)
|
||||||
|
) {
|
||||||
|
const objValue = value as any;
|
||||||
|
if (objValue.type) metadata.type = objValue.type;
|
||||||
|
if (objValue.mimeType) metadata.mimeType = objValue.mimeType;
|
||||||
|
if (objValue.filename) metadata.filename = objValue.filename;
|
||||||
|
if (objValue.language) metadata.language = objValue.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = globalRegistry.getRenderer(value, metadata);
|
||||||
|
if (renderer) {
|
||||||
|
items.push({
|
||||||
|
key: `item-${index}`,
|
||||||
|
value,
|
||||||
|
metadata,
|
||||||
|
renderer,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const textRenderer = globalRegistry
|
||||||
|
.getAllRenderers()
|
||||||
|
.find((r) => r.name === "TextRenderer");
|
||||||
|
if (textRenderer) {
|
||||||
|
items.push({
|
||||||
|
key: `item-${index}`,
|
||||||
|
value:
|
||||||
|
typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
||||||
|
metadata,
|
||||||
|
renderer: textRenderer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExecutionEntries = (
|
||||||
|
result: NodeExecutionResult,
|
||||||
|
dataType: NodeDataType,
|
||||||
|
) => {
|
||||||
|
const data = dataType === "input" ? result.input_data : result.output_data;
|
||||||
|
return Object.entries(data || {});
|
||||||
|
};
|
||||||
@@ -7,15 +7,18 @@ export const useNodeOutput = (nodeId: string) => {
|
|||||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const nodeExecutionResult = useNodeStore(
|
const latestResult = useNodeStore(
|
||||||
useShallow((state) => state.getNodeExecutionResult(nodeId)),
|
useShallow((state) => state.getLatestNodeExecutionResult(nodeId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const inputData = nodeExecutionResult?.input_data;
|
const latestInputData = useNodeStore(
|
||||||
|
useShallow((state) => state.getLatestNodeInputData(nodeId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const latestOutputData: Record<string, Array<any>> = useNodeStore(
|
||||||
|
useShallow((state) => state.getLatestNodeOutputData(nodeId) || {}),
|
||||||
|
);
|
||||||
|
|
||||||
const outputData: Record<string, Array<any>> = {
|
|
||||||
...nodeExecutionResult?.output_data,
|
|
||||||
};
|
|
||||||
const handleCopy = async (key: string, value: any) => {
|
const handleCopy = async (key: string, value: any) => {
|
||||||
try {
|
try {
|
||||||
const text = JSON.stringify(value, null, 2);
|
const text = JSON.stringify(value, null, 2);
|
||||||
@@ -35,11 +38,12 @@ export const useNodeOutput = (nodeId: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outputData,
|
latestOutputData,
|
||||||
inputData,
|
latestInputData,
|
||||||
copiedKey,
|
copiedKey,
|
||||||
handleCopy,
|
handleCopy,
|
||||||
executionResultId: nodeExecutionResult?.node_exec_id,
|
executionResultId: latestResult?.node_exec_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||||
import {
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
useNodeStore,
|
|
||||||
NodeResolutionData,
|
|
||||||
} from "@/app/(platform)/build/stores/nodeStore";
|
|
||||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||||
import {
|
import {
|
||||||
useSubAgentUpdate,
|
useSubAgentUpdate,
|
||||||
@@ -13,6 +10,7 @@ import {
|
|||||||
} from "@/app/(platform)/build/hooks/useSubAgentUpdate";
|
} from "@/app/(platform)/build/hooks/useSubAgentUpdate";
|
||||||
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
|
||||||
import { CustomNodeData } from "../../CustomNode";
|
import { CustomNodeData } from "../../CustomNode";
|
||||||
|
import { NodeResolutionData } from "@/app/(platform)/build/stores/types";
|
||||||
|
|
||||||
// Stable empty set to avoid creating new references in selectors
|
// Stable empty set to avoid creating new references in selectors
|
||||||
const EMPTY_SET: Set<string> = new Set();
|
const EMPTY_SET: Set<string> = new Set();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
import { NodeResolutionData } from "@/app/(platform)/build/stores/nodeStore";
|
import { NodeResolutionData } from "@/app/(platform)/build/stores/types";
|
||||||
import { RJSFSchema } from "@rjsf/utils";
|
import { RJSFSchema } from "@rjsf/utils";
|
||||||
|
|
||||||
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export const accumulateExecutionData = (
|
||||||
|
accumulated: Record<string, unknown[]>,
|
||||||
|
data: Record<string, unknown> | undefined,
|
||||||
|
) => {
|
||||||
|
if (!data) return { ...accumulated };
|
||||||
|
const next = { ...accumulated };
|
||||||
|
Object.entries(data).forEach(([key, values]) => {
|
||||||
|
const nextValues = Array.isArray(values) ? values : [values];
|
||||||
|
if (next[key]) {
|
||||||
|
next[key] = [...next[key], ...nextValues];
|
||||||
|
} else {
|
||||||
|
next[key] = [...nextValues];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
import { Node } from "@/app/api/__generated__/models/node";
|
import { Node } from "@/app/api/__generated__/models/node";
|
||||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||||
|
import { NodeExecutionResultInputData } from "@/app/api/__generated__/models/nodeExecutionResultInputData";
|
||||||
|
import { NodeExecutionResultOutputData } from "@/app/api/__generated__/models/nodeExecutionResultOutputData";
|
||||||
import { useHistoryStore } from "./historyStore";
|
import { useHistoryStore } from "./historyStore";
|
||||||
import { useEdgeStore } from "./edgeStore";
|
import { useEdgeStore } from "./edgeStore";
|
||||||
import { BlockUIType } from "../components/types";
|
import { BlockUIType } from "../components/types";
|
||||||
@@ -18,31 +20,10 @@ import {
|
|||||||
ensurePathExists,
|
ensurePathExists,
|
||||||
parseHandleIdToPath,
|
parseHandleIdToPath,
|
||||||
} from "@/components/renderers/InputRenderer/helpers";
|
} from "@/components/renderers/InputRenderer/helpers";
|
||||||
import { IncompatibilityInfo } from "../hooks/useSubAgentUpdate/types";
|
import { accumulateExecutionData } from "./helpers";
|
||||||
|
import { NodeResolutionData } from "./types";
|
||||||
|
|
||||||
// Resolution mode data stored per node
|
|
||||||
export type NodeResolutionData = {
|
|
||||||
incompatibilities: IncompatibilityInfo;
|
|
||||||
// The NEW schema from the update (what we're updating TO)
|
|
||||||
pendingUpdate: {
|
|
||||||
input_schema: Record<string, unknown>;
|
|
||||||
output_schema: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
// The OLD schema before the update (what we're updating FROM)
|
|
||||||
// Needed to merge and show removed inputs during resolution
|
|
||||||
currentSchema: {
|
|
||||||
input_schema: Record<string, unknown>;
|
|
||||||
output_schema: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
// The full updated hardcoded values to apply when resolution completes
|
|
||||||
pendingHardcodedValues: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Minimum movement (in pixels) required before logging position change to history
|
|
||||||
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
|
||||||
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||||
|
|
||||||
// Track initial positions when drag starts (outside store to avoid re-renders)
|
|
||||||
const dragStartPositions: Record<string, XYPosition> = {};
|
const dragStartPositions: Record<string, XYPosition> = {};
|
||||||
|
|
||||||
let dragStartState: { nodes: CustomNode[]; edges: CustomEdge[] } | null = null;
|
let dragStartState: { nodes: CustomNode[]; edges: CustomEdge[] } | null = null;
|
||||||
@@ -52,6 +33,15 @@ type NodeStore = {
|
|||||||
nodeCounter: number;
|
nodeCounter: number;
|
||||||
setNodeCounter: (nodeCounter: number) => void;
|
setNodeCounter: (nodeCounter: number) => void;
|
||||||
nodeAdvancedStates: Record<string, boolean>;
|
nodeAdvancedStates: Record<string, boolean>;
|
||||||
|
|
||||||
|
latestNodeInputData: Record<string, NodeExecutionResultInputData | undefined>;
|
||||||
|
latestNodeOutputData: Record<
|
||||||
|
string,
|
||||||
|
NodeExecutionResultOutputData | undefined
|
||||||
|
>;
|
||||||
|
accumulatedNodeInputData: Record<string, Record<string, unknown[]>>;
|
||||||
|
accumulatedNodeOutputData: Record<string, Record<string, unknown[]>>;
|
||||||
|
|
||||||
setNodes: (nodes: CustomNode[]) => void;
|
setNodes: (nodes: CustomNode[]) => void;
|
||||||
onNodesChange: (changes: NodeChange<CustomNode>[]) => void;
|
onNodesChange: (changes: NodeChange<CustomNode>[]) => void;
|
||||||
addNode: (node: CustomNode) => void;
|
addNode: (node: CustomNode) => void;
|
||||||
@@ -72,12 +62,26 @@ type NodeStore = {
|
|||||||
|
|
||||||
updateNodeStatus: (nodeId: string, status: AgentExecutionStatus) => void;
|
updateNodeStatus: (nodeId: string, status: AgentExecutionStatus) => void;
|
||||||
getNodeStatus: (nodeId: string) => AgentExecutionStatus | undefined;
|
getNodeStatus: (nodeId: string) => AgentExecutionStatus | undefined;
|
||||||
|
cleanNodesStatuses: () => void;
|
||||||
|
|
||||||
updateNodeExecutionResult: (
|
updateNodeExecutionResult: (
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
result: NodeExecutionResult,
|
result: NodeExecutionResult,
|
||||||
) => void;
|
) => void;
|
||||||
getNodeExecutionResult: (nodeId: string) => NodeExecutionResult | undefined;
|
getNodeExecutionResults: (nodeId: string) => NodeExecutionResult[];
|
||||||
|
getLatestNodeInputData: (
|
||||||
|
nodeId: string,
|
||||||
|
) => NodeExecutionResultInputData | undefined;
|
||||||
|
getLatestNodeOutputData: (
|
||||||
|
nodeId: string,
|
||||||
|
) => NodeExecutionResultOutputData | undefined;
|
||||||
|
getAccumulatedNodeInputData: (nodeId: string) => Record<string, unknown[]>;
|
||||||
|
getAccumulatedNodeOutputData: (nodeId: string) => Record<string, unknown[]>;
|
||||||
|
getLatestNodeExecutionResult: (
|
||||||
|
nodeId: string,
|
||||||
|
) => NodeExecutionResult | undefined;
|
||||||
|
clearAllNodeExecutionResults: () => void;
|
||||||
|
|
||||||
getNodeBlockUIType: (nodeId: string) => BlockUIType;
|
getNodeBlockUIType: (nodeId: string) => BlockUIType;
|
||||||
hasWebhookNodes: () => boolean;
|
hasWebhookNodes: () => boolean;
|
||||||
|
|
||||||
@@ -122,6 +126,10 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
|||||||
nodeCounter: 0,
|
nodeCounter: 0,
|
||||||
setNodeCounter: (nodeCounter) => set({ nodeCounter }),
|
setNodeCounter: (nodeCounter) => set({ nodeCounter }),
|
||||||
nodeAdvancedStates: {},
|
nodeAdvancedStates: {},
|
||||||
|
latestNodeInputData: {},
|
||||||
|
latestNodeOutputData: {},
|
||||||
|
accumulatedNodeInputData: {},
|
||||||
|
accumulatedNodeOutputData: {},
|
||||||
incrementNodeCounter: () =>
|
incrementNodeCounter: () =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
nodeCounter: state.nodeCounter + 1,
|
nodeCounter: state.nodeCounter + 1,
|
||||||
@@ -317,17 +325,162 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
|||||||
return get().nodes.find((n) => n.id === nodeId)?.data?.status;
|
return get().nodes.find((n) => n.id === nodeId)?.data?.status;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNodeExecutionResult: (nodeId: string, result: NodeExecutionResult) => {
|
cleanNodesStatuses: () => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
nodes: state.nodes.map((n) =>
|
nodes: state.nodes.map((n) => ({
|
||||||
n.id === nodeId
|
...n,
|
||||||
? { ...n, data: { ...n.data, nodeExecutionResult: result } }
|
data: { ...n.data, status: undefined },
|
||||||
: n,
|
})),
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
getNodeExecutionResult: (nodeId: string) => {
|
|
||||||
return get().nodes.find((n) => n.id === nodeId)?.data?.nodeExecutionResult;
|
updateNodeExecutionResult: (nodeId: string, result: NodeExecutionResult) => {
|
||||||
|
set((state) => {
|
||||||
|
let latestNodeInputData = state.latestNodeInputData;
|
||||||
|
let latestNodeOutputData = state.latestNodeOutputData;
|
||||||
|
let accumulatedNodeInputData = state.accumulatedNodeInputData;
|
||||||
|
let accumulatedNodeOutputData = state.accumulatedNodeOutputData;
|
||||||
|
|
||||||
|
const nodes = state.nodes.map((n) => {
|
||||||
|
if (n.id !== nodeId) return n;
|
||||||
|
|
||||||
|
const existingResults = n.data.nodeExecutionResults || [];
|
||||||
|
const duplicateIndex = existingResults.findIndex(
|
||||||
|
(r) => r.node_exec_id === result.node_exec_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicateIndex !== -1) {
|
||||||
|
const oldResult = existingResults[duplicateIndex];
|
||||||
|
const inputDataChanged =
|
||||||
|
JSON.stringify(oldResult.input_data) !==
|
||||||
|
JSON.stringify(result.input_data);
|
||||||
|
const outputDataChanged =
|
||||||
|
JSON.stringify(oldResult.output_data) !==
|
||||||
|
JSON.stringify(result.output_data);
|
||||||
|
|
||||||
|
if (!inputDataChanged && !outputDataChanged) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedResults = [...existingResults];
|
||||||
|
updatedResults[duplicateIndex] = result;
|
||||||
|
|
||||||
|
const recomputedAccumulatedInput = updatedResults.reduce(
|
||||||
|
(acc, r) => accumulateExecutionData(acc, r.input_data),
|
||||||
|
{} as Record<string, unknown[]>,
|
||||||
|
);
|
||||||
|
const recomputedAccumulatedOutput = updatedResults.reduce(
|
||||||
|
(acc, r) => accumulateExecutionData(acc, r.output_data),
|
||||||
|
{} as Record<string, unknown[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mostRecentResult = updatedResults[updatedResults.length - 1];
|
||||||
|
latestNodeInputData = {
|
||||||
|
...latestNodeInputData,
|
||||||
|
[nodeId]: mostRecentResult.input_data,
|
||||||
|
};
|
||||||
|
latestNodeOutputData = {
|
||||||
|
...latestNodeOutputData,
|
||||||
|
[nodeId]: mostRecentResult.output_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
accumulatedNodeInputData = {
|
||||||
|
...accumulatedNodeInputData,
|
||||||
|
[nodeId]: recomputedAccumulatedInput,
|
||||||
|
};
|
||||||
|
accumulatedNodeOutputData = {
|
||||||
|
...accumulatedNodeOutputData,
|
||||||
|
[nodeId]: recomputedAccumulatedOutput,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
data: {
|
||||||
|
...n.data,
|
||||||
|
nodeExecutionResults: updatedResults,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedNodeInputData = {
|
||||||
|
...accumulatedNodeInputData,
|
||||||
|
[nodeId]: accumulateExecutionData(
|
||||||
|
accumulatedNodeInputData[nodeId] || {},
|
||||||
|
result.input_data,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
accumulatedNodeOutputData = {
|
||||||
|
...accumulatedNodeOutputData,
|
||||||
|
[nodeId]: accumulateExecutionData(
|
||||||
|
accumulatedNodeOutputData[nodeId] || {},
|
||||||
|
result.output_data,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
latestNodeInputData = {
|
||||||
|
...latestNodeInputData,
|
||||||
|
[nodeId]: result.input_data,
|
||||||
|
};
|
||||||
|
latestNodeOutputData = {
|
||||||
|
...latestNodeOutputData,
|
||||||
|
[nodeId]: result.output_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
data: {
|
||||||
|
...n.data,
|
||||||
|
nodeExecutionResults: [...existingResults, result],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
latestNodeInputData,
|
||||||
|
latestNodeOutputData,
|
||||||
|
accumulatedNodeInputData,
|
||||||
|
accumulatedNodeOutputData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getNodeExecutionResults: (nodeId: string) => {
|
||||||
|
return (
|
||||||
|
get().nodes.find((n) => n.id === nodeId)?.data?.nodeExecutionResults || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getLatestNodeInputData: (nodeId: string) => {
|
||||||
|
return get().latestNodeInputData[nodeId];
|
||||||
|
},
|
||||||
|
getLatestNodeOutputData: (nodeId: string) => {
|
||||||
|
return get().latestNodeOutputData[nodeId];
|
||||||
|
},
|
||||||
|
getAccumulatedNodeInputData: (nodeId: string) => {
|
||||||
|
return get().accumulatedNodeInputData[nodeId] || {};
|
||||||
|
},
|
||||||
|
getAccumulatedNodeOutputData: (nodeId: string) => {
|
||||||
|
return get().accumulatedNodeOutputData[nodeId] || {};
|
||||||
|
},
|
||||||
|
getLatestNodeExecutionResult: (nodeId: string) => {
|
||||||
|
const results =
|
||||||
|
get().nodes.find((n) => n.id === nodeId)?.data?.nodeExecutionResults ||
|
||||||
|
[];
|
||||||
|
return results.length > 0 ? results[results.length - 1] : undefined;
|
||||||
|
},
|
||||||
|
clearAllNodeExecutionResults: () => {
|
||||||
|
set((state) => ({
|
||||||
|
nodes: state.nodes.map((n) => ({
|
||||||
|
...n,
|
||||||
|
data: {
|
||||||
|
...n.data,
|
||||||
|
nodeExecutionResults: [],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
latestNodeInputData: {},
|
||||||
|
latestNodeOutputData: {},
|
||||||
|
accumulatedNodeInputData: {},
|
||||||
|
accumulatedNodeOutputData: {},
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
getNodeBlockUIType: (nodeId: string) => {
|
getNodeBlockUIType: (nodeId: string) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { IncompatibilityInfo } from "../hooks/useSubAgentUpdate/types";
|
||||||
|
|
||||||
|
export type NodeResolutionData = {
|
||||||
|
incompatibilities: IncompatibilityInfo;
|
||||||
|
pendingUpdate: {
|
||||||
|
input_schema: Record<string, unknown>;
|
||||||
|
output_schema: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
currentSchema: {
|
||||||
|
input_schema: Record<string, unknown>;
|
||||||
|
output_schema: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
pendingHardcodedValues: Record<string, unknown>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user