feat(frontend): add UI for sticky notes on new builder (#11123)

Currently, the new builder doesn’t support sticky notes. We’re rendering
them as normal nodes with an input, which is why I’ve added a UI for
this.

<img width="1512" height="982" alt="Screenshot 2025-10-08 at 4 12 58 PM"
src="https://github.com/user-attachments/assets/be716e45-71c6-4cc4-81ba-97313426222f"
/>

To add sticky notes, go to the search menu of the block menu and search
for “Note block”. Then, add them from there.

### Changes 🏗️
- Updated CustomNodeData to include uiType.
- Conditional rendering in CustomNode based on uiType.
- Added a custom sticky note UI component called `StickyNoteBlock.tsx`.
- Adjusted FormCreator and FieldTemplate to pass and utilize uiType.
- Enhanced TextInputWidget to render differently based on uiType.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Able to attach sticky notes to the builder.
- [x] Able to accurately capture data while writing on sticky notes and
data is persistent also
This commit is contained in:
Abhimanyu Yadav
2025-10-09 12:18:19 +05:30
committed by GitHub
parent 7982c34450
commit ff72343035
7 changed files with 116 additions and 4 deletions

View File

@@ -9,6 +9,8 @@ import { preprocessInputSchema } from "../processors/input-schema-pre-processor"
import { OutputHandler } from "./OutputHandler";
import { useNodeStore } from "../../../stores/nodeStore";
import { cn } from "@/lib/utils";
import { BlockUIType } from "../../types";
import { StickyNoteBlock } from "./StickyNoteBlock";
export type CustomNodeData = {
hardcodedValues: {
@@ -18,6 +20,7 @@ export type CustomNodeData = {
description: string;
inputSchema: RJSFSchema;
outputSchema: RJSFSchema;
uiType: BlockUIType;
};
export type CustomNode = XYNode<CustomNodeData, "custom">;
@@ -29,6 +32,10 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
);
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
if (data.uiType === BlockUIType.NOTE) {
return <StickyNoteBlock selected={selected} data={data} id={id} />;
}
return (
<div
className={cn(
@@ -51,6 +58,7 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
<FormCreator
jsonSchema={preprocessInputSchema(data.inputSchema)}
nodeId={id}
uiType={data.uiType}
/>
</div>

View File

@@ -7,9 +7,18 @@ import { fields } from "./fields";
import { templates } from "./templates";
import { uiSchema } from "./uiSchema";
import { useNodeStore } from "../../../stores/nodeStore";
import { BlockUIType } from "../../types";
export const FormCreator = React.memo(
({ jsonSchema, nodeId }: { jsonSchema: RJSFSchema; nodeId: string }) => {
({
jsonSchema,
nodeId,
uiType,
}: {
jsonSchema: RJSFSchema;
nodeId: string;
uiType: BlockUIType;
}) => {
const updateNodeData = useNodeStore((state) => state.updateNodeData);
const handleChange = ({ formData }: any) => {
updateNodeData(nodeId, { hardcodedValues: formData });
@@ -22,7 +31,7 @@ export const FormCreator = React.memo(
fields={fields}
templates={templates}
widgets={widgets}
formContext={{ nodeId: nodeId }}
formContext={{ nodeId: nodeId, uiType: uiType }}
onChange={handleChange}
uiSchema={uiSchema}
/>

View File

@@ -0,0 +1,54 @@
import { useMemo } from "react";
import { FormCreator } from "./FormCreator";
import { preprocessInputSchema } from "../processors/input-schema-pre-processor";
import { CustomNodeData } from "./CustomNode";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
type StickyNoteBlockType = {
selected: boolean;
data: CustomNodeData;
id: string;
};
export const StickyNoteBlock = ({ data, id }: StickyNoteBlockType) => {
const { angle, color } = useMemo(() => {
const hash = id.split("").reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
const colors = [
"bg-orange-200",
"bg-red-200",
"bg-yellow-200",
"bg-green-200",
"bg-blue-200",
"bg-purple-200",
"bg-pink-200",
];
return {
angle: (hash % 7) - 3,
color: colors[Math.abs(hash) % colors.length],
};
}, [id]);
return (
<div
className={cn(
"relative h-76 w-76 p-4 text-black shadow-[rgba(0,0,0,0.3)_-2px_5px_5px_0px]",
color,
)}
style={{ transform: `rotate(${angle}deg)` }}
>
<Text variant="h3" className="tracking-tight text-slate-800">
Notes #{id}
</Text>
<FormCreator
jsonSchema={preprocessInputSchema(data.inputSchema)}
nodeId={id}
uiType={data.uiType}
/>
</div>
);
};

View File

@@ -20,6 +20,7 @@ import {
toDisplayName,
} from "../fields/CredentialField/helpers";
import { cn } from "@/lib/utils";
import { BlockUIType } from "@/lib/autogpt-server-api";
const FieldTemplate: React.FC<FieldTemplateProps> = ({
id,
@@ -65,6 +66,10 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
const { displayType, colorClass } = getTypeDisplayInfo(schema);
if (formContext.uiType === BlockUIType.NOTE) {
return <div className="w-full space-y-1">{children}</div>;
}
return (
<div className="mt-4 w-[400px] space-y-1">
{label && schema.type && (

View File

@@ -1,9 +1,12 @@
import { WidgetProps } from "@rjsf/utils";
import { InputType, mapJsonSchemaTypeToInputType } from "../helpers";
import { Input } from "@/components/atoms/Input/Input";
import { BlockUIType } from "@/lib/autogpt-server-api/types";
export const TextInputWidget = (props: WidgetProps) => {
const { schema } = props;
const { schema, formContext } = props;
const { uiType } = formContext as { uiType: BlockUIType };
const mapped = mapJsonSchemaTypeToInputType(schema);
type InputConfig = {
@@ -50,6 +53,25 @@ export const TextInputWidget = (props: WidgetProps) => {
return props.onChange(config.handleChange(v));
};
if (uiType === BlockUIType.NOTE) {
return (
<Input
id={props.id}
hideLabel={true}
type={"textarea"}
label={""}
size="small"
wrapperClassName="mb-0"
value={props.value ?? ""}
className="!h-[230px] resize-none rounded-none border-none bg-transparent p-0 placeholder:text-black/60 focus:ring-0"
onChange={handleChange}
placeholder={"Write your note here..."}
required={props.required}
disabled={props.disabled}
/>
);
}
return (
<Input
id={props.id}
@@ -59,7 +81,7 @@ export const TextInputWidget = (props: WidgetProps) => {
size="small"
wrapperClassName="mb-0"
value={props.value ?? ""}
onChange={handleChange as any}
onChange={handleChange}
placeholder={schema.placeholder || config.placeholder}
required={props.required}
disabled={props.disabled}

View File

@@ -1,5 +1,6 @@
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
import { CustomNodeData } from "./FlowEditor/nodes/CustomNode";
import { BlockUIType } from "./types";
export const convertBlockInfoIntoCustomNodeData = (block: BlockInfo) => {
const customNodeData: CustomNodeData = {
@@ -8,6 +9,7 @@ export const convertBlockInfoIntoCustomNodeData = (block: BlockInfo) => {
description: block.description,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
uiType: block.uiType as BlockUIType,
};
return customNodeData;
};

View File

@@ -0,0 +1,12 @@
// Currently I am using it, but we will transfer it to the backend, so we can have automated created types
export enum BlockUIType {
STANDARD = "Standard",
INPUT = "Input",
OUTPUT = "Output",
NOTE = "Note",
WEBHOOK = "Webhook",
WEBHOOK_MANUAL = "Webhook (manual)",
AGENT = "Agent",
AI = "AI",
AYRSHARE = "Ayrshare",
}