diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx index 0d6c0dc68863..85d60356c216 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx @@ -9,7 +9,10 @@ import { CustomParameterLabel, getCustomParameterTitle, } from "@/customization/components/custom-parameter"; -import { LANGFLOW_AGENTIC_EXPERIENCE } from "@/customization/feature-flags"; +import { + ENABLE_INSPECTION_PANEL, + LANGFLOW_AGENTIC_EXPERIENCE, +} from "@/customization/feature-flags"; import { useIsAutoLogin } from "@/hooks/use-is-auto-login"; import useAuthStore from "@/stores/authStore"; import { cn } from "@/utils/utils"; @@ -226,27 +229,28 @@ export default function NodeInputField({ /> - {data.node?.template[name] !== undefined && ( - - )} + {(!ENABLE_INSPECTION_PANEL || !optionalHandle) && + data.node?.template[name] !== undefined && ( + + )} ); diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx index a3f84fbc6ea9..df0016361f00 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx @@ -38,6 +38,7 @@ import { nodeColorsName } from "../../../../utils/styleUtils"; import HandleRenderComponent from "../handleRenderComponent"; import OutputComponent from "../OutputComponent"; import OutputModal from "../outputModal"; +import { ENABLE_INSPECTION_PANEL } from "@/customization/feature-flags"; const _EyeIcon = memo( ({ hidden, className }: { hidden: boolean; className: string }) => ( @@ -305,7 +306,9 @@ function NodeOutputField({ colors={colors} setFilterEdge={setFilterEdge} showNode={showNode} - testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`} + testIdComplement={`${data?.type?.toLowerCase()}-${ + showNode ? "shownode" : "noshownode" + }`} colorName={loopInputColorName} /> ); @@ -335,7 +338,9 @@ function NodeOutputField({ colors={colors} setFilterEdge={setFilterEdge} showNode={showNode} - testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`} + testIdComplement={`${data?.type?.toLowerCase()}-${ + showNode ? "shownode" : "noshownode" + }`} colorName={ data.node?.outputs?.[index].allows_loop ? loopInputColorName @@ -412,42 +417,44 @@ function NodeOutputField({ /> - -
- - +
+ {}} - id={data?.type} - /> - - {looping && ( - - Looping - - )} -
- + nodeId={flowPoolId} + outputName={internalOutputName} + > + {}} + id={data?.type} + /> +
+ {looping && ( + + Looping + + )} +
+
+ )} {Handle} diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx index d219722160b2..426c6f2fff33 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx @@ -5,6 +5,7 @@ import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field"; import getFieldTitle from "@/CustomNodes/utils/get-field-title"; import { scapedJSONStringfy } from "@/utils/reactflowUtils"; import NodeInputField from "../NodeInputField"; +import { ENABLE_INSPECTION_PANEL } from "@/customization/feature-flags"; const RenderInputParameters = ({ data, @@ -31,11 +32,31 @@ const RenderInputParameters = ({ const shownTemplateFields = useMemo(() => { return templateFields.filter((templateField) => { const template = data.node?.template[templateField]; - return ( - template?.show && - !template?.advanced && - !(template?.tool_mode && isToolMode) - ); + + if (!ENABLE_INSPECTION_PANEL) { + return ( + template?.show && + !template?.advanced && + !(template?.tool_mode && isToolMode) + ); + } + + // Basic visibility check + if ( + !template?.show || + template?.advanced || + (template?.tool_mode && isToolMode) + ) { + return false; + } + + // Only show fields that have handles (input_types) + const hasHandle = template.input_types && template.input_types.length > 0; + if (!hasHandle) { + return false; + } + + return true; }); }, [templateFields, data.node?.template, isToolMode]); diff --git a/src/frontend/src/customization/feature-flags.ts b/src/frontend/src/customization/feature-flags.ts index 9efa82c663a8..0e7225566109 100644 --- a/src/frontend/src/customization/feature-flags.ts +++ b/src/frontend/src/customization/feature-flags.ts @@ -17,6 +17,7 @@ export const ENABLE_IMAGE_ON_PLAYGROUND = false; export const ENABLE_MCP = true; export const ENABLE_MCP_NOTICE = false; export const ENABLE_KNOWLEDGE_BASES = false; +export const ENABLE_INSPECTION_PANEL = true; export const ENABLE_MCP_COMPOSER = import.meta.env.LANGFLOW_MCP_COMPOSER_ENABLED === "true"; diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx new file mode 100644 index 000000000000..e323a0b106f8 --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx @@ -0,0 +1,191 @@ +import { useMemo } from "react"; +import { AssistantButton } from "@/components/common/assistant"; +import IconComponent from "@/components/common/genericIconComponent"; +import ShadTooltip from "@/components/common/shadTooltipComponent"; +import { + CustomParameterComponent, + CustomParameterLabel, + getCustomParameterTitle, +} from "@/customization/components/custom-parameter"; +import { LANGFLOW_AGENTIC_EXPERIENCE } from "@/customization/feature-flags"; +import { useIsAutoLogin } from "@/hooks/use-is-auto-login"; +import useAuthStore from "@/stores/authStore"; +import useFlowStore from "@/stores/flowStore"; +import { useTypesStore } from "@/stores/typesStore"; +import type { NodeInputFieldComponentType } from "@/types/components"; +import { cn } from "@/utils/utils"; +import { + DEFAULT_TOOLSET_PLACEHOLDER, + FLEX_VIEW_TYPES, + ICON_STROKE_WIDTH, +} from "@/constants/constants"; +import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; +import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value"; +import useFetchDataOnMount from "@/CustomNodes/hooks/use-fetch-data-on-mount"; +import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; +import NodeInputInfo from "@/CustomNodes/GenericNode/components/NodeInputInfo"; + +export default function InspectionPanelField({ + id, + data, + title, + name = "", + required = false, + info = "", + showNode, + isToolMode = false, + proxy, +}: Omit< + NodeInputFieldComponentType, + | "colors" + | "tooltipTitle" + | "type" + | "optionalHandle" + | "colorName" + | "lastInput" +>) { + const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const isAutoLogin = useIsAutoLogin(); + const shouldDisplayApiKey = isAuthenticated && !isAutoLogin; + + const { currentFlowId, currentFlowName } = useFlowStore((state) => ({ + currentFlowId: state.currentFlow?.id, + currentFlowName: state.currentFlow?.name, + })); + + const myData = useTypesStore((state) => state.data); + const postTemplateValue = usePostTemplateValue({ + node: data.node!, + nodeId: data.id, + parameterId: name, + }); + + const { handleNodeClass } = useHandleNodeClass(data.id); + + const { handleOnNewValue } = useHandleOnNewValue({ + node: data.node!, + nodeId: data.id, + name, + }); + + const nodeInformationMetadata = useMemo(() => { + return { + flowId: currentFlowId ?? "", + nodeType: data?.type?.toLowerCase() ?? "", + flowName: currentFlowName ?? "", + isAuth: shouldDisplayApiKey!, + variableName: name, + }; + }, [data?.node?.id, shouldDisplayApiKey, name]); + + useFetchDataOnMount( + data.node!, + data.id, + handleNodeClass, + name, + postTemplateValue, + ); + + const template = data.node?.template[name]; + const type = template?.type; + const isFlexView = FLEX_VIEW_TYPES.includes(type ?? ""); + + return ( +
+
+
+
+ {proxy ? ( + {proxy.id}}> + + {getCustomParameterTitle({ + title, + nodeId: data.id, + isFlexView, + required, + })} + + + ) : ( +
+ + {getCustomParameterTitle({ + title, + nodeId: data.id, + isFlexView, + required, + })} + +
+ )} +
+ {info !== "" && ( + }> +
+ +
+
+ )} +
+ {LANGFLOW_AGENTIC_EXPERIENCE && + data.node?.template[name]?.ai_enabled && ( + + )} +
+ +
+ + {data.node?.template[name] !== undefined && ( + + )} +
+
+ ); +} + +// Made with Bob diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelFields.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelFields.tsx new file mode 100644 index 000000000000..2d2fa470df96 --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelFields.tsx @@ -0,0 +1,87 @@ +import { useMemo } from "react"; +import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field"; +import getFieldTitle from "@/CustomNodes/utils/get-field-title"; +import type { NodeDataType } from "@/types/flow"; +import { LANGFLOW_SUPPORTED_TYPES } from "@/constants/constants"; +import InspectionPanelField from "./InspectionPanelField"; + +interface InspectionPanelFieldsProps { + data: NodeDataType; +} + +export default function InspectionPanelFields({ + data, +}: InspectionPanelFieldsProps) { + // Get all fields in one list - show ALL fields in Inspection Panel + const allFields = useMemo(() => { + return Object.keys(data.node?.template || {}) + .filter((templateField) => { + const template = data.node?.template[templateField]; + + // Filter out fields that shouldn't be shown + if ( + templateField.charAt(0) === "_" || + !template?.show || + (templateField === "code" && template.type === "code") || + (templateField.includes("code") && template.proxy) + ) { + return false; + } + + // Filter out fields that are just handles (HandleInput type) + // These are fields that only serve as connection points + if (template._input_type === "HandleInput") { + return false; + } + + return true; + }) + .sort((a, b) => + sortToolModeFields( + a, + b, + data.node!.template, + data.node?.field_order ?? [], + false, + ), + ); + }, [data.node?.template, data.node?.field_order]); + + if (allFields.length === 0) { + return ( +
+ No fields available +
+ ); + } + + return ( +
+ {allFields.map((templateField: string) => { + const template = data.node?.template[templateField]; + + return ( + + ); + })} +
+ ); +} + +// Made with Bob diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx new file mode 100644 index 000000000000..5f67e7f9a2bf --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx @@ -0,0 +1,125 @@ +import { useState, useCallback, useMemo } from "react"; +import { NodeIcon } from "@/CustomNodes/GenericNode/components/nodeIcon"; +import IconComponent from "@/components/common/genericIconComponent"; +import ShadTooltip from "@/components/common/shadTooltipComponent"; +import { Button } from "@/components/ui/button"; +import CodeAreaModal from "@/modals/codeAreaModal"; +import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; +import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; +import type { NodeDataType } from "@/types/flow"; +import { ToolbarButton } from "../../nodeToolbarComponent/components/toolbar-button"; +import { useShortcutsStore } from "@/stores/shortcuts"; + +interface InspectionPanelHeaderProps { + data: NodeDataType; + onClose?: () => void; +} + +export default function InspectionPanelHeader({ + data, + onClose, +}: InspectionPanelHeaderProps) { + const [openCodeModal, setOpenCodeModal] = useState(false); + const { handleNodeClass } = useHandleNodeClass(data.id); + const { handleOnNewValue } = useHandleOnNewValue({ + node: data.node!, + nodeId: data.id, + name: "code", + }); + + const hasCode = useMemo( + () => Object.keys(data.node!.template).includes("code"), + [data.node], + ); + + const handleOpenCode = useCallback(() => { + if (hasCode) { + setOpenCodeModal(true); + } + }, [hasCode]); + + // Wrapper to match CodeAreaModal's expected signature + const handleSetValue = useCallback( + (value: string) => { + handleOnNewValue({ value }); + }, + [handleOnNewValue], + ); + + const shortcuts = useShortcutsStore((state) => state.shortcuts); + + const isCustomComponent = useMemo(() => { + const isCustom = data.type === "CustomComponent" && !data.node?.edited; + if (isCustom) { + data.node.edited = true; + } + return isCustom; + }, [data.type, data.node]); + + return ( + <> +
+
+ +
+ + {data.node?.display_name ?? data.type} + +
+
+
+ {hasCode && ( + + + s.name.toLowerCase().startsWith("code"), + )} + dataTestId="code-button-modal" + /> + + )} + {onClose && ( + + + + )} +
+
+ + {hasCode && openCodeModal && ( +
+ { + handleNodeClass(apiClassType, type); + }} + nodeClass={data.node} + value={data.node?.template?.code?.value ?? ""} + componentId={data.id} + > + <> + +
+ )} + + ); +} + +// Made with Bob diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelLogs.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelLogs.tsx new file mode 100644 index 000000000000..b743889e6b33 --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelLogs.tsx @@ -0,0 +1,78 @@ +import { useMemo } from "react"; +import type { NodeDataType } from "@/types/flow"; +import useFlowStore from "@/stores/flowStore"; +import SwitchOutputView from "@/CustomNodes/GenericNode/components/outputModal/components/switchOutputView"; +import { getGroupOutputNodeId } from "@/utils/reactflowUtils"; + +interface InspectionPanelLogsProps { + data: NodeDataType; +} + +export default function InspectionPanelLogs({ + data, +}: InspectionPanelLogsProps) { + const flowPool = useFlowStore((state) => state.flowPool); + + // Get all outputs from the node + const outputs = useMemo(() => { + return data.node?.outputs?.filter((output) => !output.hidden) ?? []; + }, [data.node?.outputs]); + + // Get the first output with logs + const firstOutputWithLogs = useMemo(() => { + for (const output of outputs) { + const outputProxy = output.proxy; + let flowPoolId = data.id; + let internalOutputName = output.name; + + if (data.node?.flow && outputProxy) { + const realOutput = getGroupOutputNodeId( + data.node.flow, + outputProxy.name, + outputProxy.id, + ); + if (realOutput) { + flowPoolId = realOutput.id; + internalOutputName = realOutput.outputName; + } + } + + const flowPoolNode = + flowPool[flowPoolId]?.[(flowPool[flowPoolId]?.length ?? 1) - 1]; + + if (flowPoolNode?.data?.logs?.[internalOutputName]) { + return { + nodeId: flowPoolId, + outputName: internalOutputName, + displayName: output.display_name || output.name, + }; + } + } + return null; + }, [outputs, data.id, data.node?.flow, flowPool]); + + if (!firstOutputWithLogs) { + return ( +
+ No logs available. Please build the component first. +
+ ); + } + + return ( +
+
+ {firstOutputWithLogs.displayName} +
+
+ +
+
+ ); +} + +// Made with Bob diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx new file mode 100644 index 000000000000..e8001879bc69 --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx @@ -0,0 +1,78 @@ +import { useMemo } from "react"; +import type { NodeDataType } from "@/types/flow"; +import useFlowStore from "@/stores/flowStore"; +import SwitchOutputView from "@/CustomNodes/GenericNode/components/outputModal/components/switchOutputView"; +import { getGroupOutputNodeId } from "@/utils/reactflowUtils"; + +interface InspectionPanelOutputsProps { + data: NodeDataType; +} + +export default function InspectionPanelOutputs({ + data, +}: InspectionPanelOutputsProps) { + const flowPool = useFlowStore((state) => state.flowPool); + + // Get all outputs from the node + const outputs = useMemo(() => { + return data.node?.outputs?.filter((output) => !output.hidden) ?? []; + }, [data.node?.outputs]); + + // Get the first output with data + const firstOutputWithData = useMemo(() => { + for (const output of outputs) { + const outputProxy = output.proxy; + let flowPoolId = data.id; + let internalOutputName = output.name; + + if (data.node?.flow && outputProxy) { + const realOutput = getGroupOutputNodeId( + data.node.flow, + outputProxy.name, + outputProxy.id, + ); + if (realOutput) { + flowPoolId = realOutput.id; + internalOutputName = realOutput.outputName; + } + } + + const flowPoolNode = + flowPool[flowPoolId]?.[(flowPool[flowPoolId]?.length ?? 1) - 1]; + + if (flowPoolNode?.data?.outputs?.[internalOutputName]?.message) { + return { + nodeId: flowPoolId, + outputName: internalOutputName, + displayName: output.display_name || output.name, + }; + } + } + return null; + }, [outputs, data.id, data.node?.flow, flowPool]); + + if (!firstOutputWithData) { + return ( +
+ No output data available. Please build the component first. +
+ ); + } + + return ( +
+
+ {firstOutputWithData.displayName} +
+
+ +
+
+ ); +} + +// Made with Bob diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx new file mode 100644 index 000000000000..01bf075b439d --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx @@ -0,0 +1,93 @@ +import { Panel } from "@xyflow/react"; +import { motion, AnimatePresence } from "framer-motion"; +import { memo, useState } from "react"; +import type { AllNodeType } from "@/types/flow"; +import { cn } from "@/utils/utils"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import InspectionPanelFields from "./components/InspectionPanelFields"; +import InspectionPanelHeader from "./components/InspectionPanelHeader"; +import InspectionPanelOutputs from "./components/InspectionPanelOutputs"; +import InspectionPanelLogs from "./components/InspectionPanelLogs"; + +interface InspectionPanelProps { + selectedNode: AllNodeType | null; + isVisible: boolean; + onClose?: () => void; +} + +const InspectionPanel = memo(function InspectionPanel({ + selectedNode, + isVisible, + onClose, +}: InspectionPanelProps) { + const [activeTab, setActiveTab] = useState<"controls" | "outputs" | "logs">( + "controls", + ); + + return ( + + {isVisible && selectedNode && selectedNode.type === "genericNode" && ( + + + + + setActiveTab(value as "controls" | "outputs" | "logs") + } + className="flex flex-col flex-1 overflow-hidden" + > + + + Controls + + + Outputs + + + Logs + + + + + + + + + + + + + + + )} + + ); +}); + +export default InspectionPanel; + +// Made with Bob diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index bc678414f8df..ad2db9e2e0dd 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -22,6 +22,7 @@ import { useShallow } from "zustand/react/shallow"; import { DefaultEdge } from "@/CustomEdges"; import NoteNode from "@/CustomNodes/NoteNode"; import FlowToolbar from "@/components/core/flowToolbarComponent"; +import InspectionPanel from "@/pages/FlowPage/components/InspectionPanel"; import { COLOR_OPTIONS, DEFAULT_NOTE_SIZE, @@ -82,6 +83,7 @@ import { } from "./MemoizedComponents"; import getRandomName from "./utils/get-random-name"; import isWrappedWithClass from "./utils/is-wrapped-with-class"; +import { ENABLE_INSPECTION_PANEL } from "@/customization/feature-flags"; const nodeTypes = { genericNode: GenericNode, @@ -744,6 +746,27 @@ export default function Page({ maxZoom: MAX_ZOOM, }; + // Determine if InspectionPanel should be visible + const showInspectionPanel = + lastSelection?.nodes?.length === 1 && + lastSelection.nodes[0].type === "genericNode"; + + // Get the fresh node data from the store instead of using stale reference + const selectedNodeId = showInspectionPanel ? lastSelection.nodes[0].id : null; + const selectedNode = selectedNodeId + ? (nodes.find((n) => n.id === selectedNodeId) as AllNodeType) + : null; + + // Handler to close the inspection panel by deselecting all nodes + const handleCloseInspectionPanel = useCallback(() => { + setNodes((nds) => + nds.map((node) => ({ + ...node, + selected: false, + })), + ); + }, [setNodes]); + return (
{showCanvas ? ( @@ -758,6 +781,13 @@ export default function Page({ shadowBoxHeight={shadowBoxHeight} /> + {ENABLE_INSPECTION_PANEL && ( + + )} )} diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 1b1430a5f228..0b237972c38d 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -40,6 +40,7 @@ import ToolbarModals from "./components/toolbar-modals"; import useShortcuts from "./hooks/use-shortcuts"; import ShortcutDisplay from "./shortcutDisplay"; import ToolbarSelectItem from "./toolbarSelectItem"; +import { ENABLE_INSPECTION_PANEL } from "@/customization/feature-flags"; const NodeToolbarComponent = memo( ({ @@ -478,7 +479,7 @@ const NodeToolbarComponent = memo( const renderToolbarButtons = useMemo( () => ( <> - {hasCode && ( + {hasCode && !ENABLE_INSPECTION_PANEL && ( )} - {nodeLength > 0 && ( + {nodeLength > 0 && !ENABLE_INSPECTION_PANEL && (