feat(frontend): Add dynamic input dialog for agent execution with credential support (#11301)

### Changes 🏗️

This PR enhances the agent execution functionality by introducing a
dynamic input dialog that collects both regular inputs and credentials
before running agents.

<img width="1309" height="826" alt="Screenshot 2025-11-03 at 10 16
38 AM"
src="https://github.com/user-attachments/assets/2015da5d-055d-49c5-8e7e-31bd0fe369f4"
/>

####  New Features
- **Dynamic Input Dialog**: Added a new `RunInputDialog` component that
automatically detects when agents require inputs or credentials and
prompts users before execution
- **Credential Management**: Integrated credential input handling
directly into the execution flow, supporting various credential types
(API keys, OAuth, passwords)
- **Enhanced Run Controls**: Improved the `RunGraph` component with
better state management and visual feedback for running/stopping agents
- **Form Renderer**: Created a new unified `FormRenderer` component for
consistent input rendering across the application

#### 🔧 Refactoring
- **Input Renderer Migration**: Moved input renderer components from
FlowEditor-specific location to a shared components directory for better
reusability:
  - Migrated fields (AnyOfField, CredentialField, ObjectField)
- Migrated widgets (ArrayEditor, DateInput, SelectWidget, TextInput,
etc.)
  - Migrated templates (FieldTemplate, ArrayFieldTemplate)
- **State Management**: Enhanced `graphStore` with schemas for inputs
and credentials, including helper methods to check for their presence
- **Component Organization**: Restructured BuilderActions components for
better modularity

#### 🗑️ Cleanup
- Removed outdated FlowEditor documentation files (FORM_CREATOR.md,
README.md)
- Removed deprecated `RunGraph` and `useRunGraph` implementations from
FlowEditor
- Consolidated duplicate functionality into new shared components

#### 🎨 UI/UX Improvements
- Added gradient styling to Run/Stop button for better visual appeal
- Improved dialog layout with clear sections for Credentials and Inputs
- Enhanced form fields with size variants (small, medium, large) for
better responsiveness
- Added loading states and proper error handling during execution

### Technical Details
- The new system automatically detects input requirements from the graph
schema
- Credentials are handled separately with special UI treatment based on
credential type
- The dialog only appears when inputs or credentials are actually
required
- Execution flow: Save graph → Check for inputs/credentials → Show
dialog if needed → Execute with provided values

### 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] Create an agent without inputs and verify it runs directly without
dialog
- [x] Create an agent with input blocks and verify the dialog appears
with correct fields
- [x] Create an agent requiring credentials and verify credential
selection/creation works
  - [x] Test agent execution with both inputs and credentials
  - [x] Verify Stop Agent functionality during execution
  - [x] Test error handling for invalid inputs or missing credentials
  - [x] Verify that the dialog closes properly after submission
  - [x] Test that execution state is properly reflected in the UI
This commit is contained in:
Abhimanyu Yadav
2025-11-03 17:35:45 +05:30
committed by GitHub
parent c17a2f807d
commit 427c7eb1d4
45 changed files with 624 additions and 911 deletions

View File

@@ -1,4 +1,4 @@
import { RunGraph } from "./components/RunGraph";
import { RunGraph } from "./components/RunGraph/RunGraph";
export const BuilderActions = () => {
return (

View File

@@ -0,0 +1,45 @@
import { Button } from "@/components/atoms/Button/Button";
import { PlayIcon } from "lucide-react";
import { useRunGraph } from "./useRunGraph";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { useShallow } from "zustand/react/shallow";
import { StopIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
export const RunGraph = () => {
const {
handleRunGraph,
handleStopGraph,
isSaving,
openRunInputDialog,
setOpenRunInputDialog,
} = useRunGraph();
const isGraphRunning = useGraphStore(
useShallow((state) => state.isGraphRunning),
);
return (
<>
<Button
variant="primary"
size="large"
className={cn(
"relative min-w-44 border-none bg-gradient-to-r from-purple-500 to-pink-500 text-lg",
)}
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
>
{!isGraphRunning && !isSaving ? (
<PlayIcon className="mr-1 size-5" />
) : (
<StopIcon className="mr-1 size-5" />
)}
{isGraphRunning || isSaving ? "Stop Agent" : "Run Agent"}
</Button>
<RunInputDialog
isOpen={openRunInputDialog}
setIsOpen={setOpenRunInputDialog}
/>
</>
);
};

View File

@@ -0,0 +1,101 @@
import {
usePostV1ExecuteGraphAgent,
usePostV1StopGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useNewSaveControl } from "../../../NewControlPanel/NewSaveControl/useNewSaveControl";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { GraphExecutionMeta } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/use-agent-runs";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { useShallow } from "zustand/react/shallow";
import { useState } from "react";
export const useRunGraph = () => {
const { onSubmit: onSaveGraph, isLoading: isSaving } = useNewSaveControl({
showToast: false,
});
const { toast } = useToast();
const setIsGraphRunning = useGraphStore(
useShallow((state) => state.setIsGraphRunning),
);
const hasInputs = useGraphStore(useShallow((state) => state.hasInputs));
const hasCredentials = useGraphStore(
useShallow((state) => state.hasCredentials),
);
const [openRunInputDialog, setOpenRunInputDialog] = useState(false);
const [{ flowID, flowVersion, flowExecutionID }, setQueryStates] =
useQueryStates({
flowID: parseAsString,
flowVersion: parseAsInteger,
flowExecutionID: parseAsString,
});
const { mutateAsync: executeGraph } = usePostV1ExecuteGraphAgent({
mutation: {
onSuccess: (response) => {
const { id } = response.data as GraphExecutionMeta;
setQueryStates({
flowExecutionID: id,
});
},
onError: (error) => {
setIsGraphRunning(false);
toast({
title: (error.detail as string) ?? "An unexpected error occurred.",
description: "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const { mutateAsync: stopGraph } = usePostV1StopGraphExecution({
mutation: {
onSuccess: () => {
setIsGraphRunning(false);
},
onError: (error) => {
toast({
title: (error.detail as string) ?? "An unexpected error occurred.",
description: "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const handleRunGraph = async () => {
await onSaveGraph(undefined);
if (hasInputs() || hasCredentials()) {
setOpenRunInputDialog(true);
} else {
setIsGraphRunning(true);
await executeGraph({
graphId: flowID ?? "",
graphVersion: flowVersion || null,
data: { inputs: {}, credentials_inputs: {} },
});
}
};
const handleStopGraph = async () => {
if (!flowExecutionID) {
return;
}
await stopGraph({
graphId: flowID ?? "",
graphExecId: flowExecutionID,
});
};
return {
handleRunGraph,
handleStopGraph,
isSaving,
openRunInputDialog,
setOpenRunInputDialog,
};
};

View File

@@ -0,0 +1,107 @@
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { RJSFSchema } from "@rjsf/utils";
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { Button } from "@/components/atoms/Button/Button";
import { PlayIcon } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
import { useRunInputDialog } from "./useRunInputDialog";
export const RunInputDialog = ({
isOpen,
setIsOpen,
}: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}) => {
const hasInputs = useGraphStore((state) => state.hasInputs);
const hasCredentials = useGraphStore((state) => state.hasCredentials);
const inputSchema = useGraphStore((state) => state.inputSchema);
const credentialsSchema = useGraphStore(
(state) => state.credentialsInputSchema,
);
const {
credentialsUiSchema,
handleManualRun,
handleInputChange,
handleCredentialChange,
isExecutingGraph,
} = useRunInputDialog({ setIsOpen });
return (
<Dialog
title="Run Agent"
controlled={{
isOpen,
set: setIsOpen,
}}
styling={{ maxWidth: "700px" }}
>
<Dialog.Content>
<div className="space-y-6 p-1">
{/* Credentials Section */}
{hasCredentials() && (
<div>
<div className="mb-4">
<Text variant="h4" className="text-gray-900">
Credentials
</Text>
</div>
<div className="px-2">
<FormRenderer
jsonSchema={credentialsSchema as RJSFSchema}
handleChange={(v) => handleCredentialChange(v.formData)}
uiSchema={credentialsUiSchema}
initialValues={{}}
formContext={{
showHandles: false,
size: "large",
}}
/>
</div>
</div>
)}
{/* Inputs Section */}
{hasInputs() && (
<div>
<div className="mb-4">
<Text variant="h4" className="text-gray-900">
Inputs
</Text>
</div>
<div className="px-2">
<FormRenderer
jsonSchema={inputSchema as RJSFSchema}
handleChange={(v) => handleInputChange(v.formData)}
uiSchema={uiSchema}
initialValues={{}}
formContext={{
showHandles: false,
size: "large",
}}
/>
</div>
</div>
)}
{/* Action Button */}
<div className="flex justify-end pt-2">
<Button
variant="primary"
size="large"
className="group h-fit min-w-0 gap-2 border-none bg-gradient-to-r from-blue-600 to-purple-600 px-8 transition-all"
onClick={handleManualRun}
loading={isExecutingGraph}
>
<PlayIcon className="size-5 transition-transform group-hover:scale-110" />
<span className="font-semibold">Manual Run</span>
</Button>
</div>
</div>
</Dialog.Content>
</Dialog>
);
};

View File

@@ -0,0 +1,108 @@
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useToast } from "@/components/molecules/Toast/use-toast";
import {
CredentialsMetaInput,
GraphExecutionMeta,
} from "@/lib/autogpt-server-api";
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { useMemo, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
import { isCredentialFieldSchema } from "@/components/renderers/input-renderer/fields/CredentialField/helpers";
export const useRunInputDialog = ({
setIsOpen,
}: {
setIsOpen: (isOpen: boolean) => void;
}) => {
const credentialsSchema = useGraphStore(
(state) => state.credentialsInputSchema,
);
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const [credentialValues, setCredentialValues] = useState<
Record<string, CredentialsMetaInput>
>({});
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
flowExecutionID: parseAsString,
flowID: parseAsString,
flowVersion: parseAsInteger,
});
const setIsGraphRunning = useGraphStore(
useShallow((state) => state.setIsGraphRunning),
);
const { toast } = useToast();
const { mutateAsync: executeGraph, isPending: isExecutingGraph } =
usePostV1ExecuteGraphAgent({
mutation: {
onSuccess: (response) => {
const { id } = response.data as GraphExecutionMeta;
setQueryStates({
flowExecutionID: id,
});
setIsGraphRunning(false);
},
onError: (error) => {
setIsGraphRunning(false);
toast({
title: (error.detail as string) ?? "An unexpected error occurred.",
description: "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
// We are rendering the credentials field differently compared to other fields.
// In the node, we have the field name as "credential" - so our library catches it and renders it differently.
// But here we have a different name, something like `Firecrawl credentials`, so here we are telling the library that this field is a credential field type.
const credentialsUiSchema = useMemo(() => {
const dynamicUiSchema: any = { ...uiSchema };
if (credentialsSchema?.properties) {
Object.keys(credentialsSchema.properties).forEach((fieldName) => {
const fieldSchema = credentialsSchema.properties[fieldName];
if (isCredentialFieldSchema(fieldSchema)) {
dynamicUiSchema[fieldName] = {
...dynamicUiSchema[fieldName],
"ui:field": "credentials",
};
}
});
}
return dynamicUiSchema;
}, [credentialsSchema]);
const handleManualRun = () => {
setIsOpen(false);
setIsGraphRunning(true);
executeGraph({
graphId: flowID ?? "",
graphVersion: flowVersion || null,
data: { inputs: inputValues, credentials_inputs: credentialValues },
});
};
const handleInputChange = (inputValues: Record<string, any>) => {
setInputValues(inputValues);
};
const handleCredentialChange = (
credentialValues: Record<string, CredentialsMetaInput>,
) => {
setCredentialValues(credentialValues);
};
return {
credentialsUiSchema,
inputValues,
credentialValues,
isExecutingGraph,
handleInputChange,
handleCredentialChange,
handleManualRun,
};
};

View File

@@ -1,32 +0,0 @@
import { Button } from "@/components/atoms/Button/Button";
import { PlayIcon } from "lucide-react";
import { useRunGraph } from "./useRunGraph";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { useShallow } from "zustand/react/shallow";
import { StopIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
export const RunGraph = () => {
const { runGraph, isSaving } = useRunGraph();
const isGraphRunning = useGraphStore(
useShallow((state) => state.isGraphRunning),
);
return (
<Button
variant="primary"
size="large"
className={cn(
"relative min-w-44 border-none bg-gradient-to-r from-purple-500 to-pink-500 text-lg",
)}
onClick={() => runGraph()}
>
{!isGraphRunning && !isSaving ? (
<PlayIcon className="mr-1 size-5" />
) : (
<StopIcon className="mr-1 size-5" />
)}
{isGraphRunning || isSaving ? "Stop Agent" : "Run Agent"}
</Button>
);
};

View File

@@ -1,62 +0,0 @@
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useNewSaveControl } from "../../../NewControlPanel/NewSaveControl/useNewSaveControl";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { GraphExecutionMeta } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/use-agent-runs";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { useShallow } from "zustand/react/shallow";
export const useRunGraph = () => {
const { onSubmit: onSaveGraph, isLoading: isSaving } = useNewSaveControl({
showToast: false,
});
const { toast } = useToast();
const setIsGraphRunning = useGraphStore(
useShallow((state) => state.setIsGraphRunning),
);
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
flowID: parseAsString,
flowVersion: parseAsInteger,
flowExecutionID: parseAsString,
});
const { mutateAsync: executeGraph } = usePostV1ExecuteGraphAgent({
mutation: {
onSuccess: (response) => {
const { id } = response.data as GraphExecutionMeta;
setQueryStates({
flowExecutionID: id,
});
},
onError: (error) => {
setIsGraphRunning(false);
toast({
title: (error.detail as string) ?? "An unexpected error occurred.",
description: "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const runGraph = async () => {
setIsGraphRunning(true);
await onSaveGraph(undefined);
// Todo : We need to save graph which has inputs and credentials inputs
await executeGraph({
graphId: flowID ?? "",
graphVersion: flowVersion || null,
data: {
inputs: {},
credentials_inputs: {},
},
});
};
return {
runGraph,
isSaving,
};
};

View File

@@ -9,7 +9,7 @@ import { CustomNode } from "../nodes/CustomNode/CustomNode";
import { useCustomEdge } from "../edges/useCustomEdge";
import { useFlowRealtime } from "./useFlowRealtime";
import { GraphLoadingBox } from "./components/GraphLoadingBox";
import { BuilderActions } from "../BuilderActions/BuilderActions";
import { BuilderActions } from "../../BuilderActions/BuilderActions";
import { RunningBackground } from "./components/RunningBackground";
import { useGraphStore } from "../../../stores/graphStore";

View File

@@ -27,6 +27,9 @@ export const useFlow = () => {
const setIsGraphRunning = useGraphStore(
useShallow((state) => state.setIsGraphRunning),
);
const setGraphSchemas = useGraphStore(
useShallow((state) => state.setGraphSchemas),
);
const [{ flowID, flowVersion, flowExecutionID }] = useQueryStates({
flowID: parseAsString,
flowVersion: parseAsInteger,
@@ -83,6 +86,14 @@ export const useFlow = () => {
}, [nodes, blocks]);
useEffect(() => {
// load graph schemas
if (graph) {
setGraphSchemas(
graph.input_schema as Record<string, any> | null,
graph.credentials_input_schema as Record<string, any> | null,
);
}
// adding nodes
if (customNodes.length > 0) {
useNodeStore.getState().setNodes([]);
@@ -128,6 +139,7 @@ export const useFlow = () => {
return () => {
useNodeStore.getState().setNodes([]);
useEdgeStore.getState().setConnections([]);
useGraphStore.getState().reset();
setIsGraphRunning(false);
};
}, []);

View File

@@ -1,580 +0,0 @@
# Form Creator System
The Form Creator is a dynamic form generation system built on React JSON Schema Form (RJSF) that automatically creates interactive forms based on JSON schemas. It's the core component that powers the input handling in the FlowEditor.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [How It Works](#how-it-works)
- [Schema Processing](#schema-processing)
- [Widget System](#widget-system)
- [Field System](#field-system)
- [Template System](#template-system)
- [Customization Guide](#customization-guide)
- [Advanced Features](#advanced-features)
## Architecture Overview
The Form Creator system consists of several interconnected layers:
```
FormCreator
├── Schema Preprocessing
│ └── input-schema-pre-processor.ts
├── Widget System
│ ├── TextInputWidget
│ ├── SelectWidget
│ ├── SwitchWidget
│ └── ... (other widgets)
├── Field System
│ ├── AnyOfField
│ ├── ObjectField
│ └── CredentialsField
├── Template System
│ ├── FieldTemplate
│ └── ArrayFieldTemplate
└── UI Schema
└── uiSchema.ts
```
## How It Works
### 1. **Schema Input**
The FormCreator receives a JSON schema that defines the structure of the form:
```typescript
const schema = {
type: "object",
properties: {
message: {
type: "string",
title: "Message",
description: "Enter your message",
},
count: {
type: "number",
title: "Count",
minimum: 0,
},
},
};
```
### 2. **Schema Preprocessing**
The schema is preprocessed to ensure all properties have proper types:
```typescript
// Before preprocessing
{
"properties": {
"name": { "title": "Name" } // No type defined
}
}
// After preprocessing
// if there is no type - that means it can accept any type
{
"properties": {
"name": {
"title": "Name",
"anyOf": [
{ "type": "string" },
{ "type": "number" },
{ "type": "boolean" },
{ "type": "array", "items": { "type": "string" } },
{ "type": "object" },
{ "type": "null" }
]
}
}
}
```
### 3. **Widget Mapping**
Schema types are mapped to appropriate input widgets:
```typescript
// Schema type -> Widget mapping
"string" -> TextInputWidget
"number" -> TextInputWidget (with number type)
"boolean" -> SwitchWidget
"array" -> ArrayFieldTemplate
"object" -> ObjectField
"enum" -> SelectWidget
```
### 4. **Form Rendering**
RJSF renders the form using the mapped widgets and templates:
```typescript
<Form
schema={preprocessedSchema}
validator={validator}
fields={fields}
templates={templates}
widgets={widgets}
formContext={{ nodeId }}
onChange={handleChange}
uiSchema={uiSchema}
/>
```
## Schema Processing
### Input Schema Preprocessor
The `preprocessInputSchema` function ensures all properties have proper types:
```typescript
export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema {
// Recursively processes properties
if (processedSchema.properties) {
for (const [key, property] of Object.entries(processedSchema.properties)) {
// Add type if none exists
if (
!processedProperty.type &&
!processedProperty.anyOf &&
!processedProperty.oneOf &&
!processedProperty.allOf
) {
processedProperty.anyOf = [
{ type: "string" },
{ type: "number" },
{ type: "integer" },
{ type: "boolean" },
{ type: "array", items: { type: "string" } },
{ type: "object" },
{ type: "null" },
];
}
}
}
}
```
### Key Features
1. **Type Safety**: Ensures all properties have types
2. **Recursive Processing**: Handles nested objects and arrays
3. **Array Item Processing**: Processes array item schemas
4. **Schema Cleanup**: Removes titles and descriptions from root schema
## Widget System
Widgets are the actual input components that users interact with.
### Available Widgets
#### TextInputWidget
Handles text, number, password, and textarea inputs:
```typescript
export const TextInputWidget = (props: WidgetProps) => {
const { schema } = props;
const mapped = mapJsonSchemaTypeToInputType(schema);
const inputConfig = {
[InputType.TEXT_AREA]: {
htmlType: "textarea",
placeholder: "Enter text...",
handleChange: (v: string) => (v === "" ? undefined : v),
},
[InputType.PASSWORD]: {
htmlType: "password",
placeholder: "Enter secret text...",
handleChange: (v: string) => (v === "" ? undefined : v),
},
[InputType.NUMBER]: {
htmlType: "number",
placeholder: "Enter number value...",
handleChange: (v: string) => (v === "" ? undefined : Number(v)),
}
};
return <Input {...config} />;
};
```
#### SelectWidget
Handles dropdown and multi-select inputs:
```typescript
export const SelectWidget = (props: WidgetProps) => {
const { options, value, onChange, schema } = props;
const enumOptions = options.enumOptions || [];
const type = mapJsonSchemaTypeToInputType(schema);
if (type === InputType.MULTI_SELECT) {
return <MultiSelector values={value} onValuesChange={onChange} />;
}
return <Select value={value} onValueChange={onChange} options={enumOptions} />;
};
```
#### SwitchWidget
Handles boolean toggles:
```typescript
export function SwitchWidget(props: WidgetProps) {
const { value = false, onChange, disabled, readonly } = props;
return (
<Switch
checked={Boolean(value)}
onCheckedChange={(checked) => onChange(checked)}
disabled={disabled || readonly}
/>
);
}
```
### Widget Registration
Widgets are registered in the widgets registry:
```typescript
export const widgets: RegistryWidgetsType = {
TextWidget: TextInputWidget,
SelectWidget: SelectWidget,
CheckboxWidget: SwitchWidget,
FileWidget: FileWidget,
DateWidget: DateInputWidget,
TimeWidget: TimeInputWidget,
DateTimeWidget: DateTimeInputWidget,
};
```
## Field System
Fields handle complex data structures and provide custom rendering logic.
### AnyOfField
Handles union types and nullable fields:
```typescript
export const AnyOfField = ({ schema, formData, onChange, ...props }: FieldProps) => {
const { isNullableType, selectedType, handleTypeChange, currentTypeOption } = useAnyOfField(schema, formData, onChange);
if (isNullableType) {
return (
<div>
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
<Switch checked={isEnabled} onCheckedChange={handleNullableToggle} />
{isEnabled && renderInput(nonNull)}
</div>
);
}
return (
<div>
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
<Select value={selectedType} onValueChange={handleTypeChange} />
{renderInput(currentTypeOption)}
</div>
);
};
```
### ObjectField
Handles free-form object editing:
```typescript
export const ObjectField = (props: FieldProps) => {
const { schema, formData = {}, onChange, name, idSchema, formContext } = props;
// Use default field for fixed-schema objects
if (idSchema?.$id === "root" || !isFreeForm) {
return <DefaultObjectField {...props} />;
}
// Use custom ObjectEditor for free-form objects
return (
<ObjectEditor
id={`${name}-input`}
nodeId={nodeId}
fieldKey={fieldKey}
value={formData}
onChange={onChange}
/>
);
};
```
### Field Registration
Fields are registered in the fields registry:
```typescript
export const fields: RegistryFieldsType = {
AnyOfField: AnyOfField,
credentials: CredentialsField,
ObjectField: ObjectField,
};
```
## Template System
Templates provide custom rendering for form structure elements.
### FieldTemplate
Custom field wrapper with connection handles:
```typescript
const FieldTemplate: React.FC<FieldTemplateProps> = ({
id, label, required, description, children, schema, formContext, uiSchema
}) => {
const { isInputConnected } = useEdgeStore();
const { nodeId } = formContext;
const fieldKey = generateHandleId(id);
const isConnected = isInputConnected(nodeId, fieldKey);
return (
<div className="mt-4 w-[400px] space-y-1">
{label && schema.type && (
<label htmlFor={id} className="flex items-center gap-1">
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
<Text variant="body">{label}</Text>
<Text variant="small" className={colorClass}>({displayType})</Text>
{required && <span style={{ color: "red" }}>*</span>}
</label>
)}
{!isConnected && <div className="pl-2">{children}</div>}
</div>
);
};
```
### ArrayFieldTemplate
Custom array editing interface:
```typescript
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
const { items, canAdd, onAddClick, disabled, readonly, formContext, idSchema } = props;
const { nodeId } = formContext;
return (
<ArrayEditor
items={items}
nodeId={nodeId}
canAdd={canAdd}
onAddClick={onAddClick}
disabled={disabled}
readonly={readonly}
id={idSchema.$id}
/>
);
}
```
## Customization Guide
### Adding a Custom Widget
1. **Create the Widget Component**:
```typescript
import { WidgetProps } from "@rjsf/utils";
export const MyCustomWidget = (props: WidgetProps) => {
const { value, onChange, schema, disabled, readonly } = props;
return (
<div>
<input
value={value || ""}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || readonly}
placeholder={schema.placeholder}
/>
</div>
);
};
```
2. **Register the Widget**:
```typescript
// In widgets/index.ts
export const widgets: RegistryWidgetsType = {
// ... existing widgets
MyCustomWidget: MyCustomWidget,
};
```
3. **Use in Schema**:
```typescript
const schema = {
type: "object",
properties: {
myField: {
type: "string",
"ui:widget": "MyCustomWidget",
},
},
};
```
### Adding a Custom Field
1. **Create the Field Component**:
```typescript
import { FieldProps } from "@rjsf/utils";
export const MyCustomField = (props: FieldProps) => {
const { schema, formData, onChange, name, idSchema, formContext } = props;
return (
<div>
{/* Custom field implementation */}
</div>
);
};
```
2. **Register the Field**:
```typescript
// In fields/index.ts
export const fields: RegistryFieldsType = {
// ... existing fields
MyCustomField: MyCustomField,
};
```
3. **Use in Schema**:
```typescript
const schema = {
type: "object",
properties: {
myField: {
type: "string",
"ui:field": "MyCustomField",
},
},
};
```
### Customizing Templates
1. **Create Custom Template**:
```typescript
const MyCustomFieldTemplate: React.FC<FieldTemplateProps> = (props) => {
return (
<div className="my-custom-field">
{/* Custom template implementation */}
</div>
);
};
```
2. **Register Template**:
```typescript
// In templates/index.ts
export const templates = {
FieldTemplate: MyCustomFieldTemplate,
// ... other templates
};
```
## Advanced Features
### Connection State Management
The Form Creator integrates with the edge store to show/hide input fields based on connection state:
```typescript
const FieldTemplate = ({ id, children, formContext }) => {
const { isInputConnected } = useEdgeStore();
const { nodeId } = formContext;
const fieldKey = generateHandleId(id);
const isConnected = isInputConnected(nodeId, fieldKey);
return (
<div>
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
{!isConnected && children}
</div>
);
};
```
### Advanced Mode
Fields can be hidden/shown based on advanced mode:
```typescript
const FieldTemplate = ({ schema, formContext }) => {
const { nodeId } = formContext;
const showAdvanced = useNodeStore(
(state) => state.nodeAdvancedStates[nodeId] || false
);
if (!showAdvanced && schema.advanced === true) {
return null;
}
return <div>{/* field content */}</div>;
};
```
### Array Item Context
Array items have special context for connection handling:
```typescript
const ArrayEditor = ({ items, nodeId }) => {
return (
<div>
{items?.map((element) => {
const fieldKey = generateHandleId(id, [element.index.toString()], HandleIdType.ARRAY);
const isConnected = isInputConnected(nodeId, fieldKey);
return (
<ArrayEditorContext.Provider
value={{ isArrayItem: true, fieldKey, isConnected }}
>
{element.children}
</ArrayEditorContext.Provider>
);
})}
</div>
);
};
```
### Handle ID Generation
Handle IDs are generated based on field structure:
```typescript
// Simple field
generateHandleId("message"); // "message"
// Nested field
generateHandleId("config", ["api_key"]); // "config.api_key"
// Array item
generateHandleId("items", ["0"]); // "items_$_0"
// Key-value pair
generateHandleId("headers", ["Authorization"]); // "headers_#_Authorization"
```

View File

@@ -1,159 +0,0 @@
# FlowEditor Component
The FlowEditor is a powerful visual flow builder component built on top of React Flow that allows users to create, connect, and manage nodes in a visual workflow. It provides a comprehensive form system with dynamic input handling, connection management, and advanced features.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Store Management](#store-management)
## Architecture Overview
The FlowEditor follows a modular architecture with clear separation of concerns:
```
FlowEditor/
├── Flow.tsx # Main component
├── nodes/ # Node-related components
│ ├── CustomNode.tsx # Main node component
│ ├── FormCreator.tsx # Dynamic form generator
│ ├── fields/ # Custom field components
│ ├── widgets/ # Custom input widgets
│ ├── templates/ # RJSF templates
│ └── helpers.ts # Utility functions
├── edges/ # Edge-related components
│ ├── CustomEdge.tsx # Custom edge component
│ ├── useCustomEdge.ts # Edge management hook
│ └── helpers.ts # Edge utilities
├── handlers/ # Connection handles
│ ├── NodeHandle.tsx # Connection handle component
│ └── helpers.ts # Handle utilities
├── components/ # Shared components
│ ├── ArrayEditor/ # Array editing components
│ └── ObjectEditor/ # Object editing components
└── processors/ # Data processors
└── input-schema-pre-processor.ts
```
## Store Management
The FlowEditor uses Zustand for state management with two main stores:
### NodeStore (`useNodeStore`)
Manages all node-related state and operations.
**Key Features:**
- Node CRUD operations
- Advanced state management per node
- Form data persistence
- Node counter for unique IDs
**Usage:**
```typescript
import { useNodeStore } from "../stores/nodeStore";
// Get nodes
const nodes = useNodeStore(useShallow((state) => state.nodes));
// Add a new node
const addNode = useNodeStore((state) => state.addNode);
// Update node data
const updateNodeData = useNodeStore((state) => state.updateNodeData);
// Toggle advanced mode
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
```
**Store Methods:**
- `setNodes(nodes)` - Replace all nodes
- `addNode(node)` - Add a single node
- `addBlock(blockInfo)` - Add node from block info
- `updateNodeData(nodeId, data)` - Update node data
- `onNodesChange(changes)` - Handle node changes from React Flow
- `setShowAdvanced(nodeId, show)` - Toggle advanced mode
- `incrementNodeCounter()` - Get next node ID
### EdgeStore (`useEdgeStore`)
Manages all connection-related state and operations.
**Key Features:**
- Connection CRUD operations
- Connection validation
- Backend link conversion
- Connection state queries
**Usage:**
```typescript
import { useEdgeStore } from "../stores/edgeStore";
// Get connections
const connections = useEdgeStore((state) => state.connections);
// Add connection
const addConnection = useEdgeStore((state) => state.addConnection);
// Check if input is connected
const isInputConnected = useEdgeStore((state) => state.isInputConnected);
```
**Store Methods:**
- `setConnections(connections)` - Replace all connections
- `addConnection(conn)` - Add a new connection
- `removeConnection(edgeId)` - Remove connection by ID
- `upsertMany(conns)` - Bulk update connections
- `isInputConnected(nodeId, handle)` - Check input connection
- `isOutputConnected(nodeId, handle)` - Check output connection
- `getNodeConnections(nodeId)` - Get all connections for a node
- `getBackendLinks()` - Convert to backend format
## Form Creator System
The FormCreator is a dynamic form generator built on React JSON Schema Form (RJSF) that automatically creates forms based on JSON schemas.
### How It Works
1. **Schema Processing**: Input schemas are preprocessed to ensure all properties have types
2. **Widget Mapping**: Schema types are mapped to appropriate input widgets
3. **Field Rendering**: Custom fields handle complex data structures
4. **State Management**: Form data is automatically synced with the node store
### Key Components
#### FormCreator
```typescript
<FormCreator
jsonSchema={preprocessedSchema}
nodeId={nodeId}
/>
```
#### Custom Widgets
- `TextInputWidget` - Text, number, password inputs
- `SelectWidget` - Dropdown and multi-select
- `SwitchWidget` - Boolean toggles
- `FileWidget` - File upload
- `DateInputWidget` - Date picker
- `TimeInputWidget` - Time picker
- `DateTimeInputWidget` - DateTime picker
#### Custom Fields
- `AnyOfField` - Union type handling
- `ObjectField` - Free-form object editing
- `CredentialsField` - API credential management
#### Templates
- `FieldTemplate` - Custom field wrapper with handles
- `ArrayFieldTemplate` - Array editing interface

View File

@@ -2,7 +2,7 @@ import { beautifyString, cn } from "@/lib/utils";
import { CustomNodeData } from "./CustomNode";
import { Text } from "@/components/atoms/Text/Text";
import { FormCreator } from "../FormCreator";
import { preprocessInputSchema } from "../../processors/input-schema-pre-processor";
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
import { Switch } from "@/components/atoms/Switch/Switch";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { OutputHandler } from "../OutputHandler";
@@ -57,7 +57,7 @@ export const StandardNodeBlock = ({
</div>
</div>
{/* Input Handles */}
<div className="bg-white pb-6 pr-6">
<div className="bg-white pr-6">
<FormCreator
jsonSchema={preprocessInputSchema(data.inputSchema)}
nodeId={nodeId}

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { FormCreator } from "../FormCreator";
import { preprocessInputSchema } from "../../processors/input-schema-pre-processor";
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
import { CustomNodeData } from "./CustomNode";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";

View File

@@ -1,13 +1,9 @@
import Form from "@rjsf/core";
import validator from "@rjsf/validator-ajv8";
import { RJSFSchema } from "@rjsf/utils";
import React from "react";
import { widgets } from "./widgets";
import { fields } from "./fields";
import { templates } from "./templates";
import { uiSchema } from "./uiSchema";
import { useNodeStore } from "../../../stores/nodeStore";
import { BlockUIType } from "../../types";
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
export const FormCreator = React.memo(
({
@@ -33,16 +29,17 @@ export const FormCreator = React.memo(
const initialValues = getHardCodedValues(nodeId);
return (
<Form
schema={jsonSchema}
validator={validator}
fields={fields}
templates={templates}
widgets={widgets}
formContext={{ nodeId: nodeId, uiType: uiType }}
onChange={handleChange}
<FormRenderer
jsonSchema={jsonSchema}
handleChange={handleChange}
uiSchema={uiSchema}
formData={initialValues}
initialValues={initialValues}
formContext={{
nodeId: nodeId,
uiType: uiType,
showHandles: true,
size: "small",
}}
/>
);
},

View File

@@ -3,9 +3,43 @@ import { create } from "zustand";
interface GraphStore {
isGraphRunning: boolean;
setIsGraphRunning: (isGraphRunning: boolean) => void;
inputSchema: Record<string, any> | null;
credentialsInputSchema: Record<string, any> | null;
setGraphSchemas: (
inputSchema: Record<string, any> | null,
credentialsInputSchema: Record<string, any> | null,
) => void;
hasInputs: () => boolean;
hasCredentials: () => boolean;
reset: () => void;
}
export const useGraphStore = create<GraphStore>((set) => ({
export const useGraphStore = create<GraphStore>((set, get) => ({
isGraphRunning: false,
inputSchema: null,
credentialsInputSchema: null,
setIsGraphRunning: (isGraphRunning: boolean) => set({ isGraphRunning }),
setGraphSchemas: (inputSchema, credentialsInputSchema) =>
set({ inputSchema, credentialsInputSchema }),
hasInputs: () => {
const { inputSchema } = get();
return Object.keys(inputSchema?.properties ?? {}).length > 0;
},
hasCredentials: () => {
const { credentialsInputSchema } = get();
return Object.keys(credentialsInputSchema?.properties ?? {}).length > 0;
},
reset: () =>
set({
isGraphRunning: false,
inputSchema: null,
credentialsInputSchema: null,
}),
}));

View File

@@ -0,0 +1,51 @@
import { BlockUIType } from "@/app/(platform)/build/components/types";
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/core";
import { RJSFSchema } from "@rjsf/utils";
import { fields } from "./fields";
import { templates } from "./templates";
import { widgets } from "./widgets";
import { preprocessInputSchema } from "./utils/input-schema-pre-processor";
import { useMemo } from "react";
type FormContextType = {
nodeId?: string;
uiType?: BlockUIType;
showHandles?: boolean;
size?: "small" | "medium" | "large";
};
type FormRendererProps = {
jsonSchema: RJSFSchema;
handleChange: (formData: any) => void;
uiSchema: any;
initialValues: any;
formContext: FormContextType;
};
export const FormRenderer = ({
jsonSchema,
handleChange,
uiSchema,
initialValues,
formContext,
}: FormRendererProps) => {
const preprocessedSchema = useMemo(() => {
return preprocessInputSchema(jsonSchema);
}, [jsonSchema]);
return (
<div className={"mt-4"}>
<Form
schema={preprocessedSchema}
validator={validator}
fields={fields}
templates={templates}
widgets={widgets}
formContext={formContext}
onChange={handleChange}
uiSchema={uiSchema}
formData={initialValues}
/>
</div>
);
};

View File

@@ -4,14 +4,17 @@ import { FieldProps, RJSFSchema } from "@rjsf/utils";
import { Text } from "@/components/atoms/Text/Text";
import { Switch } from "@/components/atoms/Switch/Switch";
import { Select } from "@/components/atoms/Select/Select";
import { InputType, mapJsonSchemaTypeToInputType } from "../../helpers";
import {
InputType,
mapJsonSchemaTypeToInputType,
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import { InfoIcon } from "@phosphor-icons/react";
import { useAnyOfField } from "./useAnyOfField";
import NodeHandle from "../../../handlers/NodeHandle";
import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { generateHandleId } from "../../../handlers/helpers";
import { getTypeDisplayInfo } from "../../helpers";
import { generateHandleId } from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import merge from "lodash/merge";
import {
Tooltip,
@@ -19,6 +22,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { cn } from "@/lib/utils";
type TypeOption = {
type: string;
@@ -46,9 +50,9 @@ export const AnyOfField = ({
const handleId = generateHandleId(idSchema.$id ?? "");
const updatedFormContexrt = { ...formContext, fromAnyOf: true };
const { nodeId } = updatedFormContexrt;
const { nodeId, showHandles = true } = updatedFormContexrt;
const { isInputConnected } = useEdgeStore();
const isConnected = isInputConnected(nodeId, handleId);
const isConnected = showHandles ? isInputConnected(nodeId, handleId) : false;
const {
isNullableType,
nonNull,
@@ -124,15 +128,21 @@ export const AnyOfField = ({
const { displayType, colorClass } = getTypeDisplayInfo(nonNull);
return (
<div className="flex flex-col">
<div className="mb-0 flex flex-col">
<div className="flex items-center justify-between gap-2">
<div className="-ml-2 flex items-center gap-1">
<NodeHandle
handleId={handleId}
isConnected={isConnected}
side="left"
/>
<Text variant="body">
<div
className={cn("flex items-center gap-1", showHandles && "-ml-2")}
>
{showHandles && (
<NodeHandle
handleId={handleId}
isConnected={isConnected}
side="left"
/>
)}
<Text
variant={formContext.size === "small" ? "body" : "body-medium"}
>
{name.charAt(0).toUpperCase() + name.slice(1)}
</Text>
<Text variant="small" className={colorClass}>
@@ -147,16 +157,22 @@ export const AnyOfField = ({
/>
)}
</div>
{!isConnected && isEnabled && renderInput(nonNull)}
<div>{!isConnected && isEnabled && renderInput(nonNull)}</div>
</div>
);
}
return (
<div className="flex flex-col">
<div className="-mb-3 -ml-2 flex items-center gap-1">
<NodeHandle handleId={handleId} isConnected={isConnected} side="left" />
<Text variant="body">
<div className="mb-0 flex flex-col">
<div className={cn("flex items-center gap-1", showHandles && "-ml-2")}>
{showHandles && (
<NodeHandle
handleId={handleId}
isConnected={isConnected}
side="left"
/>
)}
<Text variant={formContext.size === "small" ? "body" : "body-medium"}>
{name.charAt(0).toUpperCase() + name.slice(1)}
</Text>
{!isConnected && (
@@ -193,6 +209,7 @@ export const AnyOfField = ({
</TooltipProvider>
)}
</div>
{!isConnected && currentTypeOption && renderInput(currentTypeOption)}
</div>
);

View File

@@ -1,8 +1,8 @@
import React from "react";
import { FieldProps } from "@rjsf/utils";
import { getDefaultRegistry } from "@rjsf/core";
import { generateHandleId } from "../../handlers/helpers";
import { ObjectEditor } from "../../components/ObjectEditor/ObjectEditor";
import { generateHandleId } from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
import { ObjectEditor } from "../widgets/ObjectEditorWidget/ObjectEditorWidget";
export const ObjectField = (props: FieldProps) => {
const {

View File

@@ -1,6 +1,6 @@
import React from "react";
import { ArrayFieldTemplateProps } from "@rjsf/utils";
import { ArrayEditor } from "../../components/ArrayEditor/ArrayEditor";
import { ArrayEditorWidget } from "../widgets/ArrayEditorWidget/ArrayEditorWidget";
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
const {
@@ -15,7 +15,7 @@ function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
const { nodeId } = formContext;
return (
<ArrayEditor
<ArrayEditorWidget
items={items}
nodeId={nodeId}
canAdd={canAdd}

View File

@@ -9,12 +9,11 @@ import {
} from "@/components/atoms/Tooltip/BaseTooltip";
import { Text } from "@/components/atoms/Text/Text";
import NodeHandle from "../../handlers/NodeHandle";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { generateHandleId } from "../../handlers/helpers";
import { getTypeDisplayInfo } from "../helpers";
import { ArrayEditorContext } from "../../components/ArrayEditor/ArrayEditorContext";
import { generateHandleId } from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import { ArrayEditorContext } from "../widgets/ArrayEditorWidget/ArrayEditorContext";
import {
isCredentialFieldSchema,
toDisplayName,
@@ -23,6 +22,7 @@ import {
import { cn } from "@/lib/utils";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { BlockUIType } from "@/lib/autogpt-server-api";
import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
const FieldTemplate: React.FC<FieldTemplateProps> = ({
id: fieldId,
@@ -35,7 +35,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
uiSchema,
}) => {
const { isInputConnected } = useEdgeStore();
const { nodeId } = formContext;
const { nodeId, showHandles = true, size = "small" } = formContext;
const showAdvanced = useNodeStore(
(state) => state.nodeAdvancedStates[nodeId] ?? false,
@@ -55,7 +55,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
handleId = arrayFieldHandleId;
}
const isConnected = isInputConnected(nodeId, handleId);
const isConnected = showHandles ? isInputConnected(nodeId, handleId) : false;
if (!showAdvanced && schema.advanced === true && !isConnected) {
return null;
@@ -78,11 +78,22 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
return <div className="w-full space-y-1">{children}</div>;
}
// Size-based styling
const shouldShowHandle =
showHandles && !suppressHandle && !fromAnyOf && !isCredential;
return (
<div className="w-[350px] space-y-1 pt-4">
<div
className={cn(
"mb-4 space-y-2",
fromAnyOf && "mb-0",
size === "small" ? "w-[350px]" : "w-full",
)}
>
{label && schema.type && (
<label htmlFor={fieldId} className="flex items-center gap-1">
{!suppressHandle && !fromAnyOf && !isCredential && (
{shouldShowHandle && (
<NodeHandle
handleId={handleId}
isConnected={isConnected}
@@ -92,7 +103,11 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
{!fromAnyOf && (
<Text
variant="body"
className={cn("line-clamp-1", isCredential && "ml-3")}
className={cn(
"line-clamp-1",
isCredential && !shouldShowHandle && "ml-3",
size == "large" && "ml-0",
)}
>
{isCredential && credentialProvider
? toDisplayName(credentialProvider) + " credentials"
@@ -123,7 +138,9 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
)}
</label>
)}
{(isAnyOf || !isConnected) && <div className="pl-2">{children}</div>}{" "}
{(isAnyOf || !isConnected) && (
<div className={cn(size === "small" ? "pl-2" : "")}>{children}</div>
)}{" "}
</div>
);
};

View File

@@ -1,9 +1,12 @@
import { ArrayFieldTemplateItemType, RJSFSchema } from "@rjsf/utils";
import { generateHandleId, HandleIdType } from "../../handlers/helpers";
import { ArrayEditorContext } from "./ArrayEditorContext";
import { Button } from "@/components/atoms/Button/Button";
import { PlusIcon, XIcon } from "@phosphor-icons/react";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import {
generateHandleId,
HandleIdType,
} from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
export interface ArrayEditorProps {
items?: ArrayFieldTemplateItemType<any, RJSFSchema, any>[];
@@ -15,7 +18,7 @@ export interface ArrayEditorProps {
id: string;
}
export const ArrayEditor = ({
export const ArrayEditorWidget = ({
items,
nodeId,
canAdd,
@@ -29,7 +32,7 @@ export const ArrayEditor = ({
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="flex-1">
<div className="max-w-[345px] flex-1">
{items?.map((element) => {
const arrayFieldHandleId = generateHandleId(
fieldId,
@@ -59,7 +62,7 @@ export const ArrayEditor = ({
<Button
type="button"
variant="secondary"
className="relative top-5"
className="relative top-2 min-w-0"
size="small"
onClick={element.onDropIndexClick(element.index)}
>

View File

@@ -3,12 +3,24 @@ import { WidgetProps } from "@rjsf/utils";
import { DateInput } from "@/components/atoms/DateInput/DateInput";
export const DateInputWidget = (props: WidgetProps) => {
const { value, onChange, disabled, readonly, placeholder, autofocus, id } =
props;
const {
value,
onChange,
disabled,
readonly,
placeholder,
autofocus,
id,
formContext,
} = props;
const { size = "small" } = formContext || {};
// Determine input size based on context
const inputSize = size === "large" ? "default" : "small";
return (
<DateInput
size="small"
size={inputSize as any}
id={id}
hideLabel={true}
label={""}

View File

@@ -2,11 +2,24 @@ import { WidgetProps } from "@rjsf/utils";
import { DateTimeInput } from "@/components/atoms/DateTimeInput/DateTimeInput";
export const DateTimeInputWidget = (props: WidgetProps) => {
const { value, onChange, disabled, readonly, placeholder, autofocus, id } =
props;
const {
value,
onChange,
disabled,
readonly,
placeholder,
autofocus,
id,
formContext,
} = props;
const { size = "small" } = formContext || {};
// Determine input size based on context
const inputSize = size === "large" ? "medium" : "small";
return (
<DateTimeInput
size="small"
size={inputSize as any}
id={id}
hideLabel={true}
label={""}

View File

@@ -5,13 +5,13 @@ import { Plus, X } from "lucide-react";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import NodeHandle from "../../handlers/NodeHandle";
import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import {
generateHandleId,
HandleIdType,
parseKeyValueHandleId,
} from "../../handlers/helpers";
} from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
export interface ObjectEditorProps {
id: string;
@@ -90,6 +90,10 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
}
});
// Note: ObjectEditor is always used in node context, so showHandles is always true
// If you need to use it in dialog context, you'll need to pass showHandles via props
const showHandles = true;
return (
<div
ref={ref}
@@ -107,11 +111,13 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
return (
<div key={idx} className="flex flex-col gap-2">
<div className="-ml-2 flex items-center gap-1">
<NodeHandle
isConnected={isDynamicPropertyConnected}
handleId={handleId}
side="left"
/>
{showHandles && (
<NodeHandle
isConnected={isDynamicPropertyConnected}
handleId={handleId}
side="left"
/>
)}
<Text variant="small" className="!text-gray-500">
#{key.trim() === "" ? "" : key}
</Text>

View File

@@ -1,5 +1,8 @@
import { WidgetProps } from "@rjsf/utils";
import { InputType, mapJsonSchemaTypeToInputType } from "../helpers";
import {
InputType,
mapJsonSchemaTypeToInputType,
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import { Select } from "@/components/atoms/Select/Select";
import {
MultiSelector,
@@ -11,9 +14,14 @@ import {
} from "@/components/__legacy__/ui/multiselect";
export const SelectWidget = (props: WidgetProps) => {
const { options, value, onChange, disabled, readonly, id } = props;
const { options, value, onChange, disabled, readonly, id, formContext } =
props;
const enumOptions = options.enumOptions || [];
const type = mapJsonSchemaTypeToInputType(props.schema);
const { size = "small" } = formContext || {};
// Determine select size based on context
const selectSize = size === "large" ? "medium" : "small";
const renderInput = () => {
if (type === InputType.MULTI_SELECT) {
@@ -44,7 +52,7 @@ export const SelectWidget = (props: WidgetProps) => {
id={id}
hideLabel={true}
disabled={disabled || readonly}
size="small"
size={selectSize as any}
value={value ?? ""}
onValueChange={onChange}
options={

View File

@@ -1,11 +1,17 @@
import { WidgetProps } from "@rjsf/utils";
import { InputType, mapJsonSchemaTypeToInputType } from "../helpers";
import {
InputType,
mapJsonSchemaTypeToInputType,
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import { Input } from "@/components/atoms/Input/Input";
import { BlockUIType } from "@/lib/autogpt-server-api/types";
export const TextInputWidget = (props: WidgetProps) => {
const { schema, formContext } = props;
const { uiType } = formContext as { uiType: BlockUIType };
const { uiType, size = "small" } = formContext as {
uiType: BlockUIType;
size?: string;
};
const mapped = mapJsonSchemaTypeToInputType(schema);
@@ -53,6 +59,9 @@ export const TextInputWidget = (props: WidgetProps) => {
return props.onChange(config.handleChange(v));
};
// Determine input size based on context
const inputSize = size === "large" ? "medium" : "small";
if (uiType === BlockUIType.NOTE) {
return (
<Input
@@ -78,7 +87,7 @@ export const TextInputWidget = (props: WidgetProps) => {
hideLabel={true}
type={config.htmlType as any}
label={""}
size="small"
size={inputSize as any}
wrapperClassName="mb-0"
value={props.value ?? ""}
onChange={handleChange}

View File

@@ -2,7 +2,13 @@ import { WidgetProps } from "@rjsf/utils";
import { TimeInput } from "@/components/atoms/TimeInput/TimeInput";
export const TimeInputWidget = (props: WidgetProps) => {
const { value, onChange, disabled, readonly, placeholder, id } = props;
const { value, onChange, disabled, readonly, placeholder, id, formContext } =
props;
const { size = "small" } = formContext || {};
// Determine input size based on context
const inputSize = size === "large" ? "medium" : "small";
return (
<TimeInput
value={value}
@@ -11,7 +17,7 @@ export const TimeInputWidget = (props: WidgetProps) => {
label={""}
id={id}
hideLabel={true}
size="small"
size={inputSize as any}
wrapperClassName="!mb-0 "
disabled={disabled || readonly}
placeholder={placeholder}