fix: readded output inspection shortcut (#6812)
* Implemented logFirstMessage function to see if the outputName is the first message possible * Only render outputs hidden if showHiddenOutputs is true * Implemented open and setOpen on OutputModal * Add open output inspection shortcut * Added data-testid to output modal * Added test for output shortcut * fixed chatinputoutputuser test * Fixed tests * fixed tests * fixed url test name * Fixed loop test * fixed similarity test * fixed generalbugs * fixed timeouts * fix general bugs * Fixed tests * Fix timeouts * Changed timeouts * fix general bugs * Fix decision flow --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Carlos Coelho <80289056+carlosrcoelho@users.noreply.github.com>
This commit is contained in:
parent
5a13f32da5
commit
6416c0f983
10 changed files with 226 additions and 17 deletions
|
|
@ -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({
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<OutputModal
|
||||
open={openOutputModal}
|
||||
setOpen={setOpenOutputModal}
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
nodeId={flowPoolId}
|
||||
outputName={internalOutputName}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,23 @@ export default function OutputModal({
|
|||
outputName,
|
||||
children,
|
||||
disabled,
|
||||
open,
|
||||
setOpen,
|
||||
}): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState<"Outputs" | "Logs">("Outputs");
|
||||
return (
|
||||
<BaseModal disable={disabled} size="large" className="z-50">
|
||||
<BaseModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
disable={disabled}
|
||||
size="large"
|
||||
className="z-50"
|
||||
>
|
||||
<BaseModal.Header description="Inspect the output of the component below.">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="flex items-center"
|
||||
data-testid={`${nodeId}-${outputName}-output-modal`}
|
||||
>
|
||||
<span className="pr-2">Component Output</span>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
|
|
|
|||
|
|
@ -548,9 +548,11 @@ function GenericNode({
|
|||
<div
|
||||
className={cn(showHiddenOutputs ? "" : "h-0 overflow-hidden")}
|
||||
>
|
||||
<div className="block">
|
||||
{renderOutputs(data.node!.outputs, "hidden")}
|
||||
</div>
|
||||
{showHiddenOutputs && (
|
||||
<div className="block">
|
||||
{renderOutputs(data.node!.outputs, "hidden")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{hiddenOutputs && hiddenOutputs.length > 0 && (
|
||||
<ShadTooltip
|
||||
|
|
|
|||
|
|
@ -469,7 +469,20 @@ export const logHasMessage = (
|
|||
if (Array.isArray(outputs) && outputs.length > 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 = (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue