Feat: Introducing Node Minimization Feature (#906)

This pull request introduces a new feature that enhances the usability
and organization of our application's flow. We've added a convenient
"Minimize Node" button, empowering users to declutter their workspace
with a single click. This feature allows users to focus on the most
relevant nodes, resulting in a more efficient and visually pleasing
workflow.
This commit is contained in:
anovazzi1 2023-09-20 18:21:37 -03:00 • committed by GitHub
commit 2459833b87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 440 additions and 175 deletions

View file

@ -6,6 +6,13 @@
text-align: center; text-align: center;
} }
.react-flow__node {
width: auto;
height: auto;
border-radius: auto;
min-width: inherit;
}
.App-logo { .App-logo {
height: 40vmin; height: 40vmin;
pointer-events: none; pointer-events: none;

View file

@ -52,6 +52,7 @@ export default function ParameterComponent({
required = false, required = false,
optionalHandle = null, optionalHandle = null,
info = "", info = "",
showNode,
}: ParameterComponentType): JSX.Element { }: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null); const refHtml = useRef<HTMLDivElement & ReactNode>(null);
@ -192,7 +193,43 @@ export default function ParameterComponent({
renderTooltips(); renderTooltips();
}, [tooltipTitle, flow]); }, [tooltipTitle, flow]);
return ( return !showNode ? (
left &&
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int") &&
!optionalHandle ? (
<></>
) : (
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
></Handle>
</ShadTooltip>
)
) : (
<div <div
ref={ref} ref={ref}
className="mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" className="mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2"

View file

@ -29,6 +29,43 @@ export default function GenericNode({
const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
const [handles, setHandles] = useState<boolean[] | []>([]);
let numberOfInputs: boolean[] = [];
function countHandles(): void {
numberOfInputs = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map((templateCamp) => {
const { template } = data.node!;
if (template[templateCamp].input_types) return true;
if (!template[templateCamp].show) return false;
switch (template[templateCamp].type) {
case "str":
return false;
case "bool":
return false;
case "float":
return false;
case "code":
return false;
case "prompt":
return false;
case "file":
return false;
case "int":
return false;
default:
return true;
}
});
setHandles(numberOfInputs);
}
useEffect(() => {
countHandles();
}, []);
// State for outline color // State for outline color
const { sseData, isBuilding } = useSSE(); const { sseData, isBuilding } = useSSE();
useEffect(() => { useEffect(() => {
@ -50,8 +87,15 @@ export default function GenericNode({
}); });
updateFlow(flow); updateFlow(flow);
} }
countHandles();
}, [data]); }, [data]);
useEffect(() => {
setTimeout(() => {
updateNodeInternals(data.id);
}, 300);
}, [showNode]);
// New useEffect to watch for changes in sseData and update validation status // New useEffect to watch for changes in sseData and update validation status
useEffect(() => { useEffect(() => {
const relevantData = sseData[data.id]; const relevantData = sseData[data.id];
@ -70,192 +114,325 @@ export default function GenericNode({
data={data} data={data}
setData={setData} setData={setData}
deleteNode={deleteNode} deleteNode={deleteNode}
setShowNode={setShowNode}
numberOfHandles={handles}
showNode={showNode}
></NodeToolbarComponent> ></NodeToolbarComponent>
</NodeToolbar> </NodeToolbar>
<div <div
className={classNames( className={classNames(
selected ? "border border-ring" : "border", selected ? "border border-ring" : "border",
" transition-transform ",
showNode
? " w-96 scale-100 transform rounded-lg duration-500 ease-in-out "
: " transform-width w-26 h-26 scale-90 transform rounded-full duration-500 ",
"generic-node-div" "generic-node-div"
)} )}
> >
{data.node?.beta && ( {data.node?.beta && showNode && (
<div className="beta-badge-wrapper"> <div className="beta-badge-wrapper">
<div className="beta-badge-content">BETA</div> <div className="beta-badge-content">BETA</div>
</div> </div>
)} )}
<div className="generic-node-div-title"> <div>
<div className="generic-node-title-arrangement"> <div
<IconComponent className={
name={name} "generic-node-div-title " +
className="generic-node-icon" (!showNode
iconColor={`${nodeColors[types[data.type]]}`} ? " relative h-24 w-24 rounded-full "
/> : " justify-between rounded-t-lg ")
<div className="generic-node-tooltip-div"> }
<ShadTooltip content={data.node?.display_name}> >
<div className="generic-node-tooltip-div text-primary">
{data.node?.display_name}
</div>
</ShadTooltip>
</div>
</div>
<div className="round-button-div">
<div>
<Tooltip
title={
isBuilding ? (
<span>Building...</span>
) : !validationStatus ? (
<span className="flex">
Build{" "}
<IconComponent
name="Zap"
className="mx-0.5 h-5 fill-build-trigger stroke-build-trigger stroke-1"
/>{" "}
flow to validate status.
</span>
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? validationStatus.params
.split("\n")
.map((line: string, index: number) => (
<div key={index}>{line}</div>
))
: ""}
</div>
)
}
>
<div className="generic-node-status-position">
<div
className={classNames(
validationStatus && validationStatus.valid
? "green-status"
: "status-build-animation",
"status-div"
)}
></div>
<div
className={classNames(
validationStatus && !validationStatus.valid
? "red-status"
: "status-build-animation",
"status-div"
)}
></div>
<div
className={classNames(
!validationStatus || isBuilding
? "yellow-status"
: "status-build-animation",
"status-div"
)}
></div>
</div>
</Tooltip>
</div>
</div>
</div>
<div
className={
"generic-node-desc " +
(data.node?.description !== "" ? "py-5" : "pb-5")
}
>
{data.node?.description !== "" && (
<div className="generic-node-desc-text">
{data.node?.description}
</div>
)}
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
key={
(data.node!.template[templateField].input_types?.join(
";"
) ?? data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[data.node?.template[templateField].type!] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(data.node.template[templateField].name)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n"
) ?? data.node?.template[templateField].type
}
required={data.node?.template[templateField].required}
id={
(data.node?.template[templateField].input_types?.join(
";"
) ?? data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
/>
) : (
<></>
)}
</div>
))}
<div <div
className={classNames( className={
Object.keys(data.node!.template).length < 1 ? "hidden" : "", "generic-node-title-arrangement rounded-full" +
"flex-max-width justify-center" (!showNode && "justify-center")
)}
>
{" "}
</div>
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
} }
tooltipTitle={data.node?.base_classes.join("\n")} >
id={[data.type, data.id, ...data.node!.base_classes].join("|")} <IconComponent
type={data.node?.base_classes.join("|")} name={name}
left={false} className={
/> "generic-node-icon " +
</> (!showNode && "absolute inset-x-6 h-12 w-12")
}
iconColor={`${nodeColors[types[data.type]]}`}
/>
{showNode && (
<div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary">
{data.node?.display_name}
</div>
</ShadTooltip>
</div>
)}
</div>
<div>
{!showNode && (
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map(
(templateField: string, idx) =>
data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && (
<ParameterComponent
key={
(data.node!.template[
templateField
].input_types?.join(";") ??
data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[
templateField
].input_types?.join("\n") ??
data.node?.template[templateField].type
}
required={
data.node?.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
showNode={showNode}
/>
)
)}
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types &&
data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
/>
</>
)}
</div>
{showNode && (
<div className="round-button-div">
<div>
<Tooltip
title={
isBuilding ? (
<span>Building...</span>
) : !validationStatus ? (
<span className="flex">
Build{" "}
<IconComponent
name="Zap"
className="mx-0.5 h-5 fill-build-trigger stroke-build-trigger stroke-1"
/>{" "}
flow to validate status.
</span>
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? validationStatus.params
.split("\n")
.map((line: string, index: number) => (
<div key={index}>{line}</div>
))
: ""}
</div>
)
}
>
<div className="generic-node-status-position">
<div
className={classNames(
validationStatus && validationStatus.valid
? "green-status"
: "status-build-animation",
"status-div"
)}
></div>
<div
className={classNames(
validationStatus && !validationStatus.valid
? "red-status"
: "status-build-animation",
"status-div"
)}
></div>
<div
className={classNames(
!validationStatus || isBuilding
? "yellow-status"
: "status-build-animation",
"status-div"
)}
></div>
</div>
</Tooltip>
</div>
</div>
)}
</div>
</div> </div>
{showNode && (
<div
className={
showNode
? "generic-node-desc " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
: ""
}
>
{data.node?.description !== "" && showNode && (
<div className="generic-node-desc-text">
{data.node?.description}
</div>
)}
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
key={
(data.node!.template[templateField].input_types?.join(
";"
) ?? data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n"
) ?? data.node?.template[templateField].type
}
required={data.node?.template[templateField].required}
id={
(data.node?.template[templateField].input_types?.join(
";"
) ?? data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
showNode={showNode}
/>
) : (
<></>
)}
</div>
))}
<div
className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center"
)}
>
{" "}
</div>
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
/>
</>
</div>
)}
</div> </div>
</> </>
); );

View file

@ -1,5 +1,5 @@
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { useReactFlow } from "reactflow"; import { useReactFlow, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
import { TabsContext } from "../../../../contexts/tabsContext"; import { TabsContext } from "../../../../contexts/tabsContext";
@ -11,6 +11,9 @@ export default function NodeToolbarComponent({
data, data,
setData, setData,
deleteNode, deleteNode,
setShowNode,
numberOfHandles,
showNode,
}: nodeToolbarPropsType): JSX.Element { }: nodeToolbarPropsType): JSX.Element {
const [nodeLength, setNodeLength] = useState( const [nodeLength, setNodeLength] = useState(
Object.keys(data.node!.template).filter( Object.keys(data.node!.template).filter(
@ -27,6 +30,16 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "int") data.node.template[templateField].type === "int")
).length ).length
); );
const updateNodeInternals = useUpdateNodeInternals();
function canMinimize() {
let countHandles: number = 0;
numberOfHandles.forEach((bool) => {
if (bool) countHandles += 1;
});
if (countHandles > 1) return false;
return true;
}
const { paste } = useContext(TabsContext); const { paste } = useContext(TabsContext);
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
@ -106,7 +119,8 @@ export default function NodeToolbarComponent({
> >
<div <div
className={classNames( className={classNames(
"relative -ml-px inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" + "relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(!canMinimize() && " rounded-r-md ") +
(nodeLength == 0 (nodeLength == 0
? " text-muted-foreground" ? " text-muted-foreground"
: " text-foreground") : " text-foreground")
@ -117,6 +131,23 @@ export default function NodeToolbarComponent({
</EditNodeModal> </EditNodeModal>
</div> </div>
</ShadTooltip> </ShadTooltip>
{canMinimize() && (
<ShadTooltip content={showNode ? "Minimize" : "Expand"} side="top">
<button
className="relative inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
onClick={(event) => {
setShowNode((prev) => !prev);
updateNodeInternals(data.id);
}}
>
<IconComponent
name={showNode ? "Minimize2" : "Maximize2"}
className="h-4 w-4"
/>
</button>
</ShadTooltip>
)}
</span> </span>
</div> </div>
</> </>

View file

@ -264,10 +264,10 @@
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4; @apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
} }
.generic-node-div { .generic-node-div {
@apply relative flex w-96 flex-col justify-center rounded-lg bg-background; @apply relative flex flex-col justify-center bg-background;
} }
.generic-node-div-title { .generic-node-div-title {
@apply flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-muted p-4; @apply flex w-full items-center gap-8 border-b bg-muted p-4;
} }
.generic-node-title-arrangement { .generic-node-title-arrangement {
@apply flex-max-width items-center truncate; @apply flex-max-width items-center truncate;

View file

@ -46,6 +46,7 @@ export type ParameterComponentType = {
dataContext?: typesContextType; dataContext?: typesContextType;
optionalHandle?: Array<String> | null; optionalHandle?: Array<String> | null;
info?: string; info?: string;
showNode?: boolean;
}; };
export type InputListComponentType = { export type InputListComponentType = {
value: string[]; value: string[];
@ -198,6 +199,7 @@ export type IconComponentProps = {
name: string; name: string;
className?: string; className?: string;
iconColor?: string; iconColor?: string;
onClick?: () => void;
}; };
export type InputProps = { export type InputProps = {
@ -422,6 +424,9 @@ export type nodeToolbarPropsType = {
data: NodeDataType; data: NodeDataType;
deleteNode: (idx: string) => void; deleteNode: (idx: string) => void;
setData: (newState: NodeDataType) => void; setData: (newState: NodeDataType) => void;
setShowNode: (boolean: any) => void;
numberOfHandles: boolean[] | [];
showNode: boolean;
}; };
export type parsedDataType = { export type parsedDataType = {

View file

@ -45,10 +45,13 @@ import {
Link, Link,
Lock, Lock,
LucideSend, LucideSend,
Maximize2,
Menu, Menu,
MessageCircle, MessageCircle,
MessageSquare, MessageSquare,
MessagesSquare, MessagesSquare,
Minimize2,
Minus,
MoonIcon, MoonIcon,
MoreHorizontal, MoreHorizontal,
Paperclip, Paperclip,
@ -62,6 +65,7 @@ import {
Settings2, Settings2,
Shield, Shield,
Sparkles, Sparkles,
Square,
SunIcon, SunIcon,
TerminalSquare, TerminalSquare,
Trash2, Trash2,
@ -310,4 +314,8 @@ export const nodeIconsLucide: iconsType = {
Unplug, Unplug,
BookMarked, BookMarked,
ChevronUp, ChevronUp,
Minus,
Square,
Minimize2,
Maximize2,
}; };