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, + }, + ); + }, +);