feat(builder): Rewrite node input UI (#7626)

- feat(builder): Rewrite & split up `NodeInputField`
  - Create `NodeObjectInputTree`
  - Create `NodeGenericInputField`
  - Create `NodeKeyValueInput`
  - Create `NodeArrayInput`
  - Create `NodeStringInput`
  - Create `NodeNumberInput`
  - Create `NodeBooleanInput`
  - Create `NodeFallbackInput`
  - Create `ClickableInput` from `renderClickableInput(..)`
  - Amend usage in `CustomNode`
  - Remove deprecated/unused styling from `flow.css` and `customnode.css`
  - Fix alignment between `NodeHandle` and `NodeInputField`
  - Split up `BlockIOSchema` & rename to `BlockIOSubSchema`
    - Create `BlockIOObjectSubSchema`
    - Create `BlockIOKVSubSchema`
    - Create `BlockIOArraySubSchema`
    - Create `BlockIOStringSubSchema`
    - Create `BlockIONumberSubSchema`
    - Create `BlockIOBooleanSubSchema`
    - Create `BlockIONullSubSchema`
  - Install `Select` component from shad/cn

- refactor(builder): Move `NodeInputField.tsx` to `node-input.tsx`
This commit is contained in:
Reinier van der Leer
2024-08-02 20:28:46 +02:00
committed by GitHub
parent ec6bae0467
commit 3c2c3e57a0
13 changed files with 836 additions and 395 deletions

View File

@@ -16,6 +16,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",

View File

@@ -5,13 +5,12 @@ import './customnode.css';
import InputModalComponent from './InputModalComponent';
import OutputModalComponent from './OutputModalComponent';
import { BlockIORootSchema, NodeExecutionResult } from '@/lib/autogpt-server-api/types';
import { BlockSchema } from '@/lib/types';
import { beautifyString, setNestedProperty } from '@/lib/utils';
import { Switch } from "@/components/ui/switch"
import NodeHandle from './NodeHandle';
import NodeInputField from './NodeInputField';
import { Copy, Trash2 } from 'lucide-react';
import { history } from './history';
import { NodeGenericInputField } from './node-input';
export type CustomNodeData = {
blockType: string;
@@ -22,8 +21,8 @@ export type CustomNodeData = {
setHardcodedValues: (values: { [key: string]: any }) => void;
connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>;
isOutputOpen: boolean;
status?: string;
output_data?: any;
status?: NodeExecutionResult["status"];
output_data?: NodeExecutionResult["output_data"];
block_id: string;
backend_id?: string;
errors?: { [key: string]: string | null };
@@ -119,7 +118,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const getValue = (key: string) => {
const keys = key.split('.');
return keys.reduce((acc, k) => (acc && acc[k] !== undefined) ? acc[k] : '', data.hardcodedValues);
return keys.reduce((acc, k) => acc && acc[k] ? acc[k] : undefined, data.hardcodedValues);
};
const isHandleConnected = (key: string) => {
@@ -248,28 +247,36 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
)}
</div>
</div>
<div className="node-content">
<div className="flex justify-between items-start gap-2">
<div>
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
const isRequired = data.inputSchema.required?.includes(key);
Object.entries(data.inputSchema.properties).map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
return (isRequired || isAdvancedOpen) && (
<div key={key} onMouseOver={() => { }}>
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} isRequired={isRequired} schema={schema} side="left" />
{!isHandleConnected(key) &&
<NodeInputField
keyName={key}
schema={schema}
value={getValue(key)}
handleInputClick={handleInputClick}
<div key={propKey} onMouseOver={() => { }}>
<NodeHandle
keyName={propKey}
isConnected={isHandleConnected(propKey)}
schema={propSchema}
side="left"
/>
{!isHandleConnected(propKey) &&
<NodeGenericInputField
className="mt-1 mb-2"
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
handleInputChange={handleInputChange}
errors={data.errors?.[key]}
/>}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={propSchema.title || beautifyString(propKey)}
/>
}
</div>
);
})}
</div>
<div>
<div className="flex-none">
{data.outputSchema && generateOutputHandles(data.outputSchema)}
</div>
</div>
@@ -296,11 +303,11 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
</div>
)}
<div className="flex items-center mt-2.5">
<Switch onCheckedChange={toggleOutput} className='custom-switch' />
<Switch className='pl-[2px]' onCheckedChange={toggleOutput} />
<span className='m-1 mr-4'>Output</span>
{hasOptionalFields() && (
<>
<Switch onCheckedChange={toggleAdvancedSettings} className='custom-switch' />
<Switch className='pl-[2px]' onCheckedChange={toggleAdvancedSettings} />
<span className='m-1'>Advanced</span>
</>
)}

View File

@@ -15,7 +15,7 @@ import ReactFlow, {
import 'reactflow/dist/style.css';
import CustomNode, { CustomNodeData } from './CustomNode';
import './flow.css';
import AutoGPTServerAPI, { Block, BlockIOSchema, Graph, NodeExecutionResult, ObjectSchema } from '@/lib/autogpt-server-api';
import AutoGPTServerAPI, { Block, BlockIOSubSchema, Graph, NodeExecutionResult } from '@/lib/autogpt-server-api';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { ChevronRight, ChevronLeft } from "lucide-react";
@@ -392,7 +392,6 @@ const FlowEditor: React.FC<{
type: 'custom',
position: { x: node.metadata.position.x, y: node.metadata.position.y },
data: {
setIsAnyModalOpen: setIsAnyModalOpen,
block_id: block.id,
blockType: block.name,
title: `${block.name} ${node.id}`,
@@ -453,7 +452,7 @@ const FlowEditor: React.FC<{
}
const getNestedData = (
schema: BlockIOSchema, values: { [key: string]: any }
schema: BlockIOSubSchema, values: { [key: string]: any }
): { [key: string]: any } => {
let inputData: { [key: string]: any } = {};
@@ -602,7 +601,7 @@ const FlowEditor: React.FC<{
// Populate errors if validation fails
validate.errors?.forEach((error) => {
// Skip error if there's an edge connected
const path = error.instancePath || error.schemaPath;
const path = 'dataPath' in error ? error.dataPath as string : error.instancePath;
const handle = path.split(/[\/.]/)[0];
if (node.data.connections.some(conn => conn.target === node.id || conn.targetHandle === handle)) {
return;

View File

@@ -1,4 +1,4 @@
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { Handle, Position } from "reactflow";
@@ -6,7 +6,7 @@ import SchemaTooltip from "./SchemaTooltip";
type HandleProps = {
keyName: string,
schema: BlockIOSchema,
schema: BlockIOSubSchema,
isConnected: boolean,
isRequired?: boolean,
side: 'left' | 'right'
@@ -23,19 +23,19 @@ const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired,
null: 'null',
};
const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === 'left' ? 'text-left' : 'text-right'}`;
const typeClass = `text-sm ${getTypeTextColor(schema.type || 'any')} ${side === 'left' ? 'text-left' : 'text-right'}`;
const label = (
<div className="flex flex-col flex-grow">
<span className="text-m text-gray-900 -mb-1 green">
{schema.title || beautifyString(keyName)}{isRequired ? '*' : ''}
</span>
<span className={typeClass}>{typeName[schema.type]}</span>
<span className={typeClass}>{typeName[schema.type] || 'any'}</span>
</div>
);
const dot = (
<div className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type) : 'bg-gray-600'} rounded-full transition-colors duration-100 group-hover:bg-gray-300`} />
<div className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type || 'any') : 'bg-gray-600'} rounded-full transition-colors duration-100 group-hover:bg-gray-300`} />
);
if (side === 'left') {
@@ -45,7 +45,7 @@ const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired,
type="target"
position={Position.Left}
id={keyName}
className='group -ml-[29px]'
className='group -ml-[26px]'
>
<div className="pointer-events-none flex items-center">
{dot}
@@ -62,7 +62,7 @@ const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired,
type="source"
position={Position.Right}
id={keyName}
className='group -mr-[29px]'
className='group -mr-[26px]'
>
<div className="pointer-events-none flex items-center">
{label}

View File

@@ -1,294 +0,0 @@
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
import { beautifyString } from "@/lib/utils";
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
type BlockInputFieldProps = {
keyName: string
schema: BlockIOSchema
parentKey?: string
value: string | Array<string> | { [key: string]: string }
handleInputClick: (key: string) => void
handleInputChange: (key: string, value: any) => void
errors?: { [key: string]: string } | string | null
}
const NodeInputField: FC<BlockInputFieldProps> = ({
keyName: key,
schema,
parentKey = '',
value,
handleInputClick,
handleInputChange,
errors
}) => {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = typeof errors === 'string' ? errors : errors?.[key] ?? "";
const displayKey = schema.title || beautifyString(key);
const [keyValuePairs, _setKeyValuePairs] = useState<{ key: string, value: string }[]>(
"additionalProperties" in schema && value
? Object.entries(value).map(([key, value]) => ({ key: key, value: value }))
: []
);
function setKeyValuePairs(newKVPairs: typeof keyValuePairs): void {
_setKeyValuePairs(newKVPairs);
handleInputChange(
fullKey,
newKVPairs.reduce((obj, { key, value }) => ({ ...obj, [key]: value }), {})
);
}
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
const className = `clickable-input ${error ? 'border-error' : ''}`;
return secret ? (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value ? <span>********</span> : <i className="text-gray-500">{placeholder}</i>}
</div>
) : (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value || <i className="text-gray-500">{placeholder}</i>}
</div>
);
};
if ("properties" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{Object.entries(schema.properties).map(([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if (schema.type === 'object' && schema.additionalProperties) {
return (
<div key={fullKey}>
<div>
{keyValuePairs.map(({ key, value }, index) => (
<div key={index} className="flex items-center w-[325px] space-x-2 mb-2">
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) => setKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value, value: value
})
)}
/>
<Input
type="text"
placeholder="Value"
value={value}
onChange={(e) => setKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key, value: e.target.value
})
)}
/>
<Button variant="ghost" className="px-2"
onClick={() => setKeyValuePairs(keyValuePairs.toSpliced(index, 1))}
>
<Cross2Icon />
</Button>
</div>
))}
<Button className="w-full"
onClick={() => setKeyValuePairs(
keyValuePairs.concat({ key: "", value: "" })
)}
>
<PlusIcon className="mr-2" /> Add Property
</Button>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
}
if ("anyOf" in schema) {
const types = schema.anyOf.map(s => "type" in s ? s.type : undefined);
if (types.includes('string') && types.includes('null')) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey} (optional)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
if ("allOf" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{"properties" in schema.allOf[0] &&
Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if ("oneOf" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{"properties" in schema.oneOf[0] &&
Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if (!("type" in schema)) {
console.warn(`Schema for input ${key} does not specify a type:`, schema);
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
switch (schema.type) {
case 'string':
if (schema.enum) {
return (
<div key={fullKey} className="input-container">
<select
value={value as string || ''}
onChange={(e) => handleInputChange(fullKey, e.target.value)}
className="select-input"
>
<option value="">Select {displayKey}</option>
{schema.enum.map((option: string) => (
<option key={option} value={option}>
{beautifyString(option)}
</option>
))}
</select>
{error && <span className="error-message">{error}</span>}
</div>
);
}
if (schema.secret) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`, true)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`)}
{error && <span className="error-message">{error}</span>}
</div>
);
case 'boolean':
return (
<div key={fullKey} className="input-container">
<select
value={value === undefined ? '' : value.toString()}
onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
className="select-input"
>
<option value="">Select {displayKey}</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
{error && <span className="error-message">{error}</span>}
</div>
);
case 'number':
case 'integer':
return (
<div key={fullKey} className="input-container">
<Input
type="number"
value={value as string || ''}
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
className={`number-input ${error ? 'border-error' : ''}`}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
case 'array':
if (schema.items && schema.items.type === 'string') {
const arrayValues = value as Array<string> || [];
return (
<div key={fullKey} className="input-container">
{arrayValues.map((item: string, index: number) => (
<div key={`${fullKey}.${index}`} className="array-item-container">
<Input
type="text"
value={item}
onChange={(e) => handleInputChange(`${fullKey}.${index}`, e.target.value)}
className="array-item-input"
/>
<Button onClick={() => handleInputChange(`${fullKey}.${index}`, '')} className="array-item-remove">
&times;
</Button>
</div>
))}
<Button onClick={() => handleInputChange(fullKey, [...arrayValues, ''])} className="array-item-add">
Add Item
</Button>
{error && <span className="error-message ml-2">{error}</span>}
</div>
);
}
return null;
default:
console.warn(`Schema for input ${key} specifies unknown type:`, schema);
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
};
export default NodeInputField;

View File

@@ -4,11 +4,11 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { Info } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
const SchemaTooltip: React.FC<{ schema: BlockIOSchema }> = ({ schema }) => {
const SchemaTooltip: React.FC<{ schema: BlockIOSubSchema }> = ({ schema }) => {
if (!schema.description) return null;
return (

View File

@@ -1,5 +1,5 @@
.custom-node {
padding: 15px;
@apply p-3;
border: 3px solid #4b5563;
border-radius: 12px;
background: #ffffff;
@@ -89,10 +89,6 @@
transform: none;
}
.input-container {
margin-bottom: 5px;
}
.clickable-input {
padding: 5px;
width: 325px;
@@ -191,29 +187,9 @@
.error-message {
color: #d9534f;
font-size: 12px;
font-size: 13px;
margin-top: 5px;
}
.object-input {
margin-left: 10px;
border-left: 1px solid #000; /* Border for nested inputs */
padding-left: 10px;
}
.nested-input {
margin-top: 5px;
}
.key-value-input {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
}
.key-value-input input {
flex-grow: 1;
margin-left: 5px;
}
/* Styles for node states */

View File

@@ -126,24 +126,3 @@ input::placeholder, textarea::placeholder {
width: 100%;
height: 600px; /* Adjust this height as needed */
}
.flow-wrapper {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.flow-controls {
position: absolute;
left: -80px;
z-index: 1001;
display: flex;
gap: 10px;
transition: transform 0.3s ease;
}
.flow-controls.open {
transform: translateX(350px);
}

View File

@@ -0,0 +1,545 @@
import { Cross2Icon, Pencil2Icon, PlusIcon } from "@radix-ui/react-icons";
import { beautifyString, cn } from "@/lib/utils";
import {
BlockIORootSchema,
BlockIOSubSchema,
BlockIOObjectSubSchema,
BlockIOKVSubSchema,
BlockIOArraySubSchema,
BlockIOStringSubSchema,
BlockIONumberSubSchema,
BlockIOBooleanSubSchema,
} from "@/lib/autogpt-server-api/types";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Switch } from "./ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
type NodeObjectInputTreeProps = {
selfKey?: string;
schema: BlockIORootSchema | BlockIOObjectSubSchema;
object?: { [key: string]: any };
handleInputClick: (key: string) => void;
handleInputChange: (key: string, value: any) => void;
errors: { [key: string]: string | undefined };
className?: string;
displayName?: string;
};
const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
selfKey = "",
schema,
object,
handleInputClick,
handleInputChange,
errors,
className,
displayName,
}) => {
object ??= ("default" in schema ? schema.default : null) ?? {};
return (
<div className={cn(className, 'flex-col')}>
{displayName && <strong>{displayName}:</strong>}
{Object.entries(schema.properties).map(([propKey, propSchema]) => {
const childKey = selfKey ? `${selfKey}.${propKey}` : propKey;
return (
<div className="flex flex-row space-y-2 w-full">
<span className="mr-2 mt-3">{propKey}</span>
<NodeGenericInputField
key={propKey}
propKey={childKey}
propSchema={propSchema}
currentValue={object[propKey]}
errors={errors}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
</div>
)
})}
</div>
);
};
export default NodeObjectInputTree;
export const NodeGenericInputField: FC<{
propKey: string;
propSchema: BlockIOSubSchema;
currentValue?: any;
errors: NodeObjectInputTreeProps["errors"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName?: string;
}> = ({
propKey,
propSchema,
currentValue,
errors,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
displayName ??= propSchema.title || beautifyString(propKey);
if ("allOf" in propSchema) {
// If this happens, that is because Pydantic wraps $refs in an allOf if the
// $ref has sibling schema properties (which isn't technically allowed),
// so there will only be one item in allOf[].
// AFAIK this should NEVER happen though, as $refs are resolved server-side.
propSchema = propSchema.allOf[0];
console.warn(`Unsupported 'allOf' in schema for '${propKey}'!`, propSchema);
}
if ("properties" in propSchema) {
return (
<NodeObjectInputTree
selfKey={propKey}
schema={propSchema}
object={currentValue}
errors={errors}
className={cn("border-l border-gray-500 pl-2", className)} // visual indent
displayName={displayName}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
/>
);
}
if ("additionalProperties" in propSchema) {
return (
<NodeKeyValueInput
selfKey={propKey}
schema={propSchema}
entries={currentValue}
errors={errors}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
}
if ("anyOf" in propSchema) { // optional items
const types = propSchema.anyOf.map((s) => ("type" in s ? s.type : undefined));
if (types.includes("string") && types.includes("null")) { // optional string
return (
<NodeStringInput
selfKey={propKey}
schema={{ ...propSchema, type: "string" } as BlockIOStringSubSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
}
}
if ("oneOf" in propSchema) {
// At the time of writing, this isn't used in the backend -> no impl. needed
console.error(`Unsupported 'oneOf' in schema for '${propKey}'!`, propSchema);
return null;
}
if (!("type" in propSchema)) {
return (
<NodeFallbackInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputClick={handleInputClick}
/>
);
}
switch (propSchema.type) {
case "string":
return (
<NodeStringInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
case "boolean":
return (
<NodeBooleanInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
case "number":
case "integer":
return (
<NodeNumberInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
/>
);
case "array":
return (
<NodeArrayInput
selfKey={propKey}
schema={propSchema}
entries={currentValue}
errors={errors}
className={className}
displayName={displayName}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
default:
console.warn(`Schema for '${propKey}' specifies unknown type:`, propSchema);
return (
<NodeFallbackInput
selfKey={propKey}
schema={propSchema}
value={currentValue}
error={errors[propKey]}
className={className}
displayName={displayName}
handleInputClick={handleInputClick}
/>
);
}
}
const NodeKeyValueInput: FC<{
selfKey: string;
schema: BlockIOKVSubSchema;
entries?: { [key: string]: string } | { [key: string]: number };
errors: { [key: string]: string | undefined };
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName?: string;
}> = ({ selfKey, entries, schema, handleInputChange, errors, className, displayName }) => {
const [keyValuePairs, setKeyValuePairs] = useState<{
key: string;
value: string | number | null;
}[]>(
Object.entries(entries ?? schema.default ?? {})
.map(([key, value]) => ({ key, value: value }))
);
function updateKeyValuePairs(newPairs: typeof keyValuePairs) {
setKeyValuePairs(newPairs);
handleInputChange(
selfKey,
newPairs.reduce((obj, { key, value }) => ({ ...obj, [key]: value }), {})
);
};
function convertValueType(value: string): string | number | null {
if (schema.additionalProperties.type == "string") return value;
if (!value) return null;
return Number(value)
}
return (
<div className={className}>
{displayName && <strong>{displayName}:</strong>}
<div>
{keyValuePairs.map(({ key, value }, index) => (
<div key={index}>
<div className="flex items-center space-x-2 mb-2">
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value,
value: value,
})
)
}
/>
<Input
type="text"
placeholder="Value"
value={value ?? ""}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key,
value: convertValueType(e.target.value),
})
)
}
/>
<Button
variant="ghost"
className="px-2"
onClick={() => updateKeyValuePairs(keyValuePairs.toSpliced(index, 1))}
>
<Cross2Icon />
</Button>
</div>
{errors[`${selfKey}.${key}`] &&
<span className="error-message">{errors[`${selfKey}.${key}`]}</span>
}
</div>
))}
<Button
className="w-full"
onClick={() =>
updateKeyValuePairs(keyValuePairs.concat({ key: "", value: "" }))
}
>
<PlusIcon className="mr-2" /> Add Property
</Button>
</div>
{errors[selfKey] && <span className="error-message">{errors[selfKey]}</span>}
</div>
);
};
const NodeArrayInput: FC<{
selfKey: string;
schema: BlockIOArraySubSchema;
entries?: string[];
errors: { [key: string]: string | undefined };
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName?: string;
}> = ({
selfKey,
schema,
entries,
errors,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
entries ??= schema.default ?? [];
return (
<div className={`input-container ${className ?? ""}`}>
{displayName && <strong>{displayName}</strong>}
{entries.map((entry: string, index: number) => {
const entryKey = `${selfKey}[${index}]`;
return (
<div key={entryKey}>
<div className="flex items-center space-x-2 mb-2">
{schema.items
? <NodeGenericInputField
propKey={entryKey}
propSchema={schema.items}
currentValue={entry}
errors={errors}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
: <NodeFallbackInput
selfKey={entryKey}
schema={schema.items}
value={entry}
error={errors[entryKey]}
displayName={displayName ?? "something"}
handleInputClick={handleInputClick}
/>
}
<Button
variant="ghost" size="icon"
onClick={() => handleInputChange(selfKey, entries.toSpliced(index, 1))}
>
<Cross2Icon />
</Button>
</div>
{errors[entryKey] &&
<span className="error-message">{errors[entryKey]}</span>
}
</div>
)
})}
<Button onClick={() => handleInputChange(selfKey, [...entries, ""])}>
<PlusIcon className="mr-2" /> Add Item
</Button>
{errors[selfKey] && <span className="error-message">{errors[selfKey]}</span>}
</div>
);
};
const NodeStringInput: FC<{
selfKey: string;
schema: BlockIOStringSubSchema;
value?: string;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName: string;
}> = ({
selfKey,
schema,
value,
error,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
return (
<div className={`input-container ${className ?? ""}`}>
{schema.enum ? (
<Select
defaultValue={value}
onValueChange={newValue => handleInputChange(selfKey, newValue)}
>
<SelectTrigger>
<SelectValue placeholder={displayName || schema.placeholder || schema.title} />
</SelectTrigger>
<SelectContent>
{schema.enum.map((option, index) => (
<SelectItem key={index} value={option}>
{beautifyString(option)}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="relative">
<Input
type="text"
id={selfKey}
value={value}
placeholder={
schema?.placeholder || `Enter ${beautifyString(displayName)}`
}
onChange={e => handleInputChange(selfKey, e.target.value)}
className="pr-8"
/>
<Button
variant="ghost" size="icon"
className="absolute inset-1 left-auto h-7 w-8 rounded-[0.25rem]"
onClick={() => handleInputClick(selfKey)}
title="Open a larger textbox input"
>
<Pencil2Icon className="m-0 p-0" />
</Button>
</div>
)}
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeNumberInput: FC<{
selfKey: string;
schema: BlockIONumberSubSchema;
value?: number;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName?: string;
}> = ({ selfKey, schema, value, error, handleInputChange, className, displayName }) => {
value ??= schema.default;
return (
<div className={`input-container ${className ?? ""}`}>
<div className="flex items-center justify-between space-x-3">
{displayName && <Label htmlFor={selfKey}>{displayName}</Label>}
<Input
type="number"
id={selfKey}
value={value}
onChange={(e) => handleInputChange(selfKey, parseFloat(e.target.value))}
/>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeBooleanInput: FC<{
selfKey: string;
schema: BlockIOBooleanSubSchema;
value?: boolean;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName: string;
}> = ({ selfKey, schema, value, error, handleInputChange, className, displayName }) => {
value ??= schema.default ?? false;
return (
<div className={`input-container ${className ?? ""}`}>
<div className="flex items-center">
<Switch checked={value} onCheckedChange={v => handleInputChange(selfKey, v)} />
<span className="ml-3">{displayName}</span>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeFallbackInput: FC<{
selfKey: string;
schema?: BlockIOSubSchema;
value: any;
error?: string;
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName: string;
}> = ({ selfKey, schema, value, error, handleInputClick, className, displayName }) => {
return (
<div className={`input-container ${className ?? ""}`}>
<ClickableInput
handleInputClick={() => handleInputClick(selfKey)}
value={value}
placeholder={
schema?.placeholder || `Enter ${beautifyString(displayName)} (Complex)`
}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const ClickableInput: FC<{
handleInputClick: () => void;
value?: string;
secret?: boolean;
placeholder?: string;
className?: string;
}> = ({
handleInputClick,
value = "",
placeholder = "",
secret = false,
className,
}) => (
<div className={`clickable-input ${className}`} onClick={handleInputClick}>
{secret
? <i className="text-gray-500">{value ? "********" : placeholder}</i>
: value || <i className="text-gray-500">{placeholder}</i>
}
</div>
);

View File

@@ -0,0 +1,164 @@
"use client"
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-neutral-200 bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-neutral-800 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-neutral-300",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-neutral-200 bg-white text-neutral-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-800", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -9,51 +9,80 @@ export type Block = {
export type BlockIORootSchema = {
type: "object";
properties: { [key: string]: BlockIOSchema };
properties: { [key: string]: BlockIOSubSchema };
required?: string[];
additionalProperties?: { type: string };
}
export type BlockIOSchema = {
export type BlockIOSubSchema =
| BlockIOSimpleTypeSubSchema
| BlockIOCombinedTypeSubSchema;
type BlockIOSimpleTypeSubSchema =
| BlockIOObjectSubSchema
| BlockIOKVSubSchema
| BlockIOArraySubSchema
| BlockIOStringSubSchema
| BlockIONumberSubSchema
| BlockIOBooleanSubSchema
| BlockIONullSubSchema;
type BlockIOSubSchemaMeta = {
title?: string;
description?: string;
placeholder?: string;
} & (BlockIOSimpleTypeSchema | BlockIOCombinedTypeSchema);
};
type BlockIOSimpleTypeSchema = {
export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
type: "object";
properties: { [key: string]: BlockIOSchema };
required?: string[];
additionalProperties?: { type: string };
} | {
properties: { [key: string]: BlockIOSubSchema };
default?: { [key: keyof BlockIOObjectSubSchema["properties"]]: any };
required?: keyof BlockIOObjectSubSchema["properties"][];
};
export type BlockIOKVSubSchema = BlockIOSubSchemaMeta & {
type: "object";
additionalProperties: { type: "string" | "number" | "integer" };
default?: { [key: string]: string | number };
};
export type BlockIOArraySubSchema = BlockIOSubSchemaMeta & {
type: "array";
items?: BlockIOSimpleTypeSchema;
} | {
items?: BlockIOSimpleTypeSubSchema;
default?: Array<string>;
};
export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
type: "string";
enum?: string[];
secret?: true;
default?: string;
} | {
};
export type BlockIONumberSubSchema = BlockIOSubSchemaMeta & {
type: "integer" | "number";
default?: number;
} | {
};
export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & {
type: "boolean";
default?: boolean;
} | {
};
export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
type: "null";
};
// At the time of writing, combined schemas only occur on the first nested level in a
// block schema. It is typed this way to make the use of these objects less tedious.
type BlockIOCombinedTypeSchema = {
allOf: [BlockIOSimpleTypeSchema];
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & ({
allOf: [BlockIOSimpleTypeSubSchema];
} | {
anyOf: BlockIOSimpleTypeSchema[];
anyOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
} | {
oneOf: BlockIOSimpleTypeSchema[];
oneOf: BlockIOSimpleTypeSubSchema[];
default?: string | number | boolean | null;
};
});
/* Mirror of autogpt_server/data/graph.py:Node */
export type Node = {

View File

@@ -41,6 +41,7 @@ export function getTypeTextColor(type: string | null): string {
array: 'text-indigo-500',
null: 'text-gray-500',
'': 'text-gray-500',
any: 'text-gray-500',
}[type] || 'text-gray-500';
}
@@ -55,6 +56,7 @@ export function getTypeBgColor(type: string | null): string {
array: 'bg-indigo-500',
null: 'bg-gray-500',
'': 'bg-gray-500',
any: 'bg-gray-500',
}[type] || 'bg-gray-500';
}
@@ -68,6 +70,7 @@ export function getTypeColor(type: string | null): string {
array: '#6366f1',
null: '#6b7280',
'': '#6b7280',
any: '#6b7280',
}[type] || '#6b7280';
}

View File

@@ -224,6 +224,11 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@radix-ui/number@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46"
integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==
"@radix-ui/primitive@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
@@ -447,6 +452,33 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-select@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.1.tgz#df05cb0b29d3deaef83b505917c4042e0e418a9f"
integrity sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==
dependencies:
"@radix-ui/number" "1.1.0"
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-collection" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-focus-guards" "1.1.0"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.0"
"@radix-ui/react-portal" "1.1.1"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.7"
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"