diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx
index c6df43a4b..7ab945713 100644
--- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx
@@ -1,5 +1,6 @@
import { Badge } from "@/components/ui/badge";
import { ICON_STROKE_WIDTH } from "@/constants/constants";
+import { useShortcutsStore } from "@/stores/shortcuts";
import { targetHandleType } from "@/types/flow";
import { useUpdateNodeInternals } from "@xyflow/react";
import { cloneDeep } from "lodash";
@@ -10,7 +11,9 @@ import {
useEffect,
useMemo,
useRef,
+ useState,
} from "react";
+import { useHotkeys } from "react-hotkeys-hook";
import ForwardedIconComponent, {
default as IconComponent,
} from "../../../../components/common/genericIconComponent";
@@ -26,6 +29,7 @@ import {
} from "../../../../utils/reactflowUtils";
import {
cn,
+ logFirstMessage,
logHasMessage,
logTypeIsError,
logTypeIsUnknown,
@@ -256,6 +260,46 @@ function NodeOutputField({
}
}, [disabledOutput, data.node?.outputs, handleUpdateOutputHide, index]);
+ const [openOutputModal, setOpenOutputModal] = useState(false);
+
+ const outputShortcutOpenable = useMemo(() => {
+ if (!displayOutputPreview || !selected) return;
+
+ const sortedEdges = edges
+ .filter((edge) => edge.source === data.id)
+ .toSorted((a, b) => {
+ const indexA =
+ data?.node?.outputs?.findIndex(
+ (output) => output.name === a.data?.sourceHandle?.name,
+ ) ?? 0;
+ const indexB =
+ data?.node?.outputs?.findIndex(
+ (output) => output.name === b.data?.sourceHandle?.name,
+ ) ?? 0;
+ return indexA - indexB;
+ });
+
+ const isFirstOutput =
+ sortedEdges[0]?.sourceHandle === scapedJSONStringfy(id);
+ const hasNoEdges = !edges.some((edge) => edge.source === data.id);
+ const isValidFirstMessage =
+ hasNoEdges && logFirstMessage(flowPoolNode?.data, internalOutputName);
+
+ if (isFirstOutput || isValidFirstMessage) {
+ return true;
+ }
+ return false;
+ }, [displayOutputPreview, edges, data.id, data?.node?.outputs, selected]);
+
+ const handleOpenOutputModal = () => {
+ if (outputShortcutOpenable) {
+ setOpenOutputModal(true);
+ }
+ };
+
+ const outputInspection = useShortcutsStore((state) => state.outputInspection);
+ useHotkeys(outputInspection, handleOpenOutputModal, { preventDefault: true });
+
const LoopHandle = useMemo(() => {
if (data.node?.outputs![index].allows_loop) {
return (
@@ -389,6 +433,8 @@ function NodeOutputField({
>
("Outputs");
return (
-
+
-
+
Component Output
diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx
index 83ad9ccd9..5b8c16ea3 100644
--- a/src/frontend/src/CustomNodes/GenericNode/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx
@@ -548,9 +548,11 @@ function GenericNode({
-
- {renderOutputs(data.node!.outputs, "hidden")}
-
+ {showHiddenOutputs && (
+
+ {renderOutputs(data.node!.outputs, "hidden")}
+
+ )}
{hiddenOutputs && hiddenOutputs.length > 0 && (
0) {
return outputs.some((outputLog) => outputLog?.message);
}
- return outputs?.message;
+ return !!outputs?.message;
+};
+
+export const logFirstMessage = (
+ data: VertexDataTypeAPI,
+ outputName: string | undefined,
+) => {
+ if (!outputName || !data?.outputs) return false;
+ for (const key of Object.keys(data.outputs)) {
+ if (logHasMessage(data, key)) {
+ return key === outputName;
+ }
+ }
+ return false;
};
export const logTypeIsUnknown = (
diff --git a/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts b/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts
index 6e4ebd8b2..38c67e40d 100644
--- a/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts
+++ b/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts
@@ -84,7 +84,7 @@ test(
timeout: 30000,
});
- await page.getByTestId("icon-TextSearchIcon").nth(4).click();
+ await page.getByTestId("icon-TextSearchIcon").nth(2).click();
await page.getByText("Restart").isHidden();
},
diff --git a/src/frontend/tests/core/integrations/decisionFlow.spec.ts b/src/frontend/tests/core/integrations/decisionFlow.spec.ts
index aafec502b..ab7db95d5 100644
--- a/src/frontend/tests/core/integrations/decisionFlow.spec.ts
+++ b/src/frontend/tests/core/integrations/decisionFlow.spec.ts
@@ -240,7 +240,7 @@ test(
.click();
await page
.getByTestId("handle-createlist-shownode-data list-right")
- .nth(2)
+ .nth(1)
.click();
await page
.getByTestId("handle-parsedata-shownode-data-left")
@@ -265,7 +265,7 @@ test(
.click();
await page
.getByTestId("handle-parsedata-shownode-message-right")
- .nth(2)
+ .nth(1)
.click();
await page
.getByTestId("handle-prompt-shownode-false_examples-left")
@@ -273,7 +273,7 @@ test(
.click();
await page
.getByTestId("handle-pass-shownode-output message-right")
- .nth(4)
+ .nth(2)
.click();
await page
.getByTestId("handle-prompt-shownode-user_message-left")
@@ -330,7 +330,7 @@ test(
.click();
await page
.getByTestId("handle-pass-shownode-output message-right")
- .nth(2)
+ .nth(1)
.click();
await page
.getByTestId("handle-chatoutput-noshownode-text-target")
diff --git a/src/frontend/tests/core/integrations/similarity.spec.ts b/src/frontend/tests/core/integrations/similarity.spec.ts
index 2dd16a30a..21cd4932e 100644
--- a/src/frontend/tests/core/integrations/similarity.spec.ts
+++ b/src/frontend/tests/core/integrations/similarity.spec.ts
@@ -195,7 +195,7 @@ test(
//connection 4
const textEmbedderOutput_1 = await page
.getByTestId("handle-textembeddercomponent-shownode-embedding data-right")
- .nth(2);
+ .nth(1);
await textEmbedderOutput_1.hover();
await page.mouse.down();
await embeddingSimilarityInput.hover();
diff --git a/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts b/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts
index 39e2470b2..55a9e1a60 100644
--- a/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts
+++ b/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts
@@ -92,7 +92,7 @@ test(
.first();
const secondBlockedHandle = page
.getByTestId("div-handle-combinetext-shownode-combined text-right")
- .nth(3);
+ .nth(1);
const thirdBlockedHandle = page
.getByTestId("div-handle-textoutput-shownode-message-right")
.first();
@@ -192,7 +192,7 @@ test(
//connection 2
const elementTextOutput1 = page
.getByTestId("handle-textinput-shownode-message-right")
- .nth(4);
+ .nth(2);
await elementTextOutput1.click();
const elementGroupInput1 = page
.getByTestId("handle-groupnode-shownode-second text-left")
@@ -202,7 +202,7 @@ test(
//connection 3
const elementTextOutput2 = page
.getByTestId("handle-textinput-shownode-message-right")
- .nth(2);
+ .nth(1);
await elementTextOutput2.click();
const elementGroupInput2 = page
diff --git a/src/frontend/tests/extended/features/loop-component.spec.ts b/src/frontend/tests/extended/features/loop-component.spec.ts
index d7342a44a..2be3cee2c 100644
--- a/src/frontend/tests/extended/features/loop-component.spec.ts
+++ b/src/frontend/tests/extended/features/loop-component.spec.ts
@@ -79,7 +79,7 @@ test(
const secondParseDataOutput = await page
.getByTestId("handle-parsedata-shownode-data list-right")
- .nth(2);
+ .nth(1);
const loopItemInput = await page
.getByTestId("handle-loopcomponent-shownode-item-left")
diff --git a/src/frontend/tests/extended/integrations/chatInputOutputUser-shard-1.spec.ts b/src/frontend/tests/extended/integrations/chatInputOutputUser-shard-1.spec.ts
index 879689739..ca239b121 100644
--- a/src/frontend/tests/extended/integrations/chatInputOutputUser-shard-1.spec.ts
+++ b/src/frontend/tests/extended/integrations/chatInputOutputUser-shard-1.spec.ts
@@ -38,10 +38,147 @@ test(
timeout: 30000,
});
- await page.getByTestId("icon-TextSearchIcon").nth(4).click();
+ await page.getByTestId("icon-TextSearchIcon").nth(2).click();
await page.getByText("Sender", { exact: true }).isVisible();
await page.getByText("Type", { exact: true }).isVisible();
await page.getByText("User", { exact: true }).last().isVisible();
},
);
+
+test(
+ "user must be able to see output inspection using 'o' shortcut",
+ { tag: ["@release", "@components"] },
+ async ({ page }) => {
+ await awaitBootstrapTest(page);
+
+ await page.getByTestId("blank-flow").click();
+
+ // Add URL component
+ await page.getByTestId("sidebar-search-input").click();
+ await page.getByTestId("sidebar-search-input").fill("url");
+ await page.waitForSelector('[data-testid="dataURL"]', {
+ timeout: 1000,
+ });
+
+ await page
+ .getByTestId("dataURL")
+ .dragTo(page.locator('//*[@id="react-flow-id"]'), {
+ targetPosition: { x: 300, y: 200 },
+ });
+
+ // Get URL node ID
+ const urlNode = await page.locator(".react-flow__node").first();
+ const urlNodeId = await urlNode.getAttribute("data-id");
+
+ // Add two chat outputs
+ await page.getByTestId("sidebar-search-input").click();
+ await page.getByTestId("sidebar-search-input").fill("chat output");
+ await page.waitForSelector('[data-testid="outputsChat Output"]', {
+ timeout: 1000,
+ });
+
+ await page
+ .getByTestId("outputsChat Output")
+ .dragTo(page.locator('//*[@id="react-flow-id"]'), {
+ targetPosition: { x: 700, y: 200 },
+ });
+
+ await page
+ .getByTestId("outputsChat Output")
+ .dragTo(page.locator('//*[@id="react-flow-id"]'), {
+ targetPosition: { x: 700, y: 400 },
+ });
+
+ // Fill URL input
+ await page
+ .getByTestId("inputlist_str_urls_0")
+ .fill("https://www.example.com");
+
+ // Connect text output to first chat output
+ const urlTextOutput = await page
+ .getByTestId("handle-url-shownode-text-right")
+ .nth(0);
+ await urlTextOutput.hover();
+ await page.mouse.down();
+ const firstChatInput = await page
+ .getByTestId("handle-chatoutput-noshownode-text-target")
+ .nth(0);
+ await firstChatInput.hover();
+ await page.mouse.up();
+
+ // Run flow and test text output inspection
+ await page.getByTestId("button_run_url").first().click();
+ await page.waitForSelector("text=built successfully", { timeout: 30000 });
+ await page.keyboard.press("o");
+ await page.waitForSelector(
+ `[data-testid="${urlNodeId}-text-output-modal"]`,
+ {
+ timeout: 1000,
+ },
+ );
+ await page.keyboard.press("Escape");
+
+ // Connect dataframe output to second chat output
+ const urlDataframeOutput = await page
+ .getByTestId("handle-url-shownode-dataframe-right")
+ .nth(0);
+ await urlDataframeOutput.hover();
+ await page.mouse.down();
+ const secondChatInput = await page
+ .getByTestId("handle-chatoutput-noshownode-text-target")
+ .nth(1);
+ await secondChatInput.hover();
+ await page.mouse.up();
+
+ // Run and verify text output is still shown
+ await page.getByTestId("button_run_url").first().click();
+ await page.waitForSelector("text=built successfully", { timeout: 30000 });
+ await page.waitForTimeout(600);
+ await page.keyboard.press("o");
+ await page.waitForSelector(
+ `[data-testid="${urlNodeId}-text-output-modal"]`,
+ {
+ timeout: 1000,
+ },
+ );
+ await page.keyboard.press("Escape");
+
+ // Remove text connection
+ const textEdge = await page.locator(".react-flow__edge").first();
+ await textEdge.click();
+ await page.keyboard.press("Backspace");
+ await page.waitForTimeout(600);
+
+ // Run and verify dataframe output is now shown
+ await page.getByTestId("button_run_url").first().click();
+ await page.waitForSelector("text=built successfully", { timeout: 30000 });
+ await page.waitForTimeout(600);
+ await page.keyboard.press("o");
+ await page.waitForSelector(
+ `[data-testid="${urlNodeId}-dataframe-output-modal"]`,
+ {
+ timeout: 3000,
+ },
+ );
+ await page.keyboard.press("Escape");
+
+ // Remove all connections
+ const dataEdge = await page.locator(".react-flow__edge").first();
+ await dataEdge.click();
+ await page.keyboard.press("Backspace");
+ await page.waitForTimeout(600);
+
+ // Run and verify data output is shown
+ await page.getByTestId("button_run_url").first().click();
+ await page.waitForSelector("text=built successfully", { timeout: 30000 });
+ await page.waitForTimeout(600);
+ await page.keyboard.press("o");
+ await page.waitForSelector(
+ `[data-testid="${urlNodeId}-data-output-modal"]`,
+ {
+ timeout: 3000,
+ },
+ );
+ },
+);