fix #3171, ensure proper form rendering for int (#3201)

This commit is contained in:
Victor Dibia
2026-01-19 23:25:12 -08:00
committed by GitHub
Unverified
parent e0b9be7e08
commit 0f29637b86
6 changed files with 296 additions and 680 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,16 @@ export { WorkflowDetailsModal } from "./workflow-details-modal";
export { WorkflowFlow } from "./workflow-flow";
export { WorkflowInputForm } from "./workflow-input-form";
export { ExecutorNode } from "./executor-node";
export { SchemaFormRenderer, validateSchemaForm, filterEmptyOptionalFields } from "./schema-form-renderer";
export {
SchemaFormRenderer,
validateSchemaForm,
filterEmptyOptionalFields,
resolveSchemaType,
isShortField,
shouldFieldBeTextarea,
getFieldColumnSpan,
detectChatMessagePattern,
} from "./schema-form-renderer";
export { CheckpointInfoModal } from "./checkpoint-info-modal";
export { RunWorkflowButton } from "./run-workflow-button";
export type { RunWorkflowButtonProps } from "./run-workflow-button";
@@ -15,10 +15,10 @@ import { ChevronDown, ChevronUp } from "lucide-react";
import type { JSONSchemaProperty } from "@/types";
// ============================================================================
// Field Type Detection (from WorkflowInputForm)
// Field Type Detection Helpers (exported for reuse)
// ============================================================================
function isShortField(fieldName: string): boolean {
export function isShortField(fieldName: string): boolean {
const shortFieldNames = [
"name",
"title",
@@ -37,7 +37,51 @@ function isShortField(fieldName: string): boolean {
return shortFieldNames.includes(fieldName.toLowerCase());
}
function shouldFieldBeTextarea(
// Helper: Resolve anyOf/oneOf union types to get the primary type
// Pydantic generates these for Optional[T] as: anyOf: [{type: T}, {type: "null"}]
export function resolveSchemaType(schema: JSONSchemaProperty): JSONSchemaProperty {
// If schema has a direct type, return as-is
if (schema.type) {
return schema;
}
// Handle anyOf (common for Optional[T])
if (schema.anyOf && schema.anyOf.length > 0) {
// Filter out null type and get the first non-null type
const nonNullTypes = schema.anyOf.filter(
(s) => s.type !== "null" && s.type !== undefined
);
if (nonNullTypes.length > 0) {
// Merge the resolved type with original schema's metadata (default, description, etc.)
return {
...nonNullTypes[0],
default: schema.default ?? nonNullTypes[0].default,
description: schema.description ?? nonNullTypes[0].description,
title: schema.title ?? nonNullTypes[0].title,
};
}
}
// Handle oneOf similarly
if (schema.oneOf && schema.oneOf.length > 0) {
const nonNullTypes = schema.oneOf.filter(
(s) => s.type !== "null" && s.type !== undefined
);
if (nonNullTypes.length > 0) {
return {
...nonNullTypes[0],
default: schema.default ?? nonNullTypes[0].default,
description: schema.description ?? nonNullTypes[0].description,
title: schema.title ?? nonNullTypes[0].title,
};
}
}
// Fallback: return original schema (will render as JSON textarea)
return schema;
}
export function shouldFieldBeTextarea(
fieldName: string,
schema: JSONSchemaProperty
): boolean {
@@ -48,7 +92,7 @@ function shouldFieldBeTextarea(
);
}
function getFieldColumnSpan(
export function getFieldColumnSpan(
fieldName: string,
schema: JSONSchemaProperty
): string {
@@ -71,10 +115,10 @@ function getFieldColumnSpan(
}
// ============================================================================
// ChatMessage Pattern Detection (from WorkflowInputForm)
// ChatMessage Pattern Detection (exported for reuse)
// ============================================================================
function detectChatMessagePattern(
export function detectChatMessagePattern(
schema: JSONSchemaProperty,
requiredFields: string[]
): boolean {
@@ -93,7 +137,7 @@ function detectChatMessagePattern(
}
// ============================================================================
// Form Field Component (from WorkflowInputForm)
// Form Field Component (internal - used by SchemaFormRenderer)
// ============================================================================
interface FormFieldProps {
@@ -107,12 +151,14 @@ interface FormFieldProps {
function FormField({
name,
schema,
schema: rawSchema,
value,
onChange,
isRequired = false,
isReadOnly = false,
}: FormFieldProps) {
// Resolve anyOf/oneOf union types (e.g., Optional[int] → int)
const schema = resolveSchemaType(rawSchema);
const { type, description, enum: enumValues, default: defaultValue } = schema;
const isTextarea = shouldFieldBeTextarea(name, schema);
@@ -356,9 +402,10 @@ export interface SchemaFormRendererProps {
values: Record<string, unknown>;
onChange: (values: Record<string, unknown>) => void;
disabled?: boolean;
readOnlyFields?: string[]; // NEW: Fields to display but not edit (for HIL)
hideFields?: string[]; // NEW: Fields to completely hide
showCollapsedByDefault?: boolean; // NEW: Control initial collapsed state
readOnlyFields?: string[]; // Fields to display but not edit (for HIL)
hideFields?: string[]; // Fields to completely hide
showCollapsedByDefault?: boolean; // Control initial collapsed state
layout?: "stack" | "grid"; // Layout mode: "stack" (vertical) or "grid" (responsive 4-col)
}
export function SchemaFormRenderer({
@@ -369,11 +416,18 @@ export function SchemaFormRenderer({
readOnlyFields = [],
hideFields = [],
showCollapsedByDefault = false,
layout = "stack",
}: SchemaFormRendererProps) {
const [showAdvancedFields, setShowAdvancedFields] = useState(
showCollapsedByDefault
);
// Container class based on layout mode
const containerClass =
layout === "grid"
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
: "space-y-3";
const properties = schema.properties || {};
const allFieldNames = Object.keys(properties).filter(
(name) => !hideFields.includes(name)
@@ -428,8 +482,12 @@ export function SchemaFormRenderer({
});
};
// Full-width class for separator and toggle button in grid mode
const fullWidthClass =
layout === "grid" ? "md:col-span-2 lg:col-span-3 xl:col-span-4" : "";
return (
<div className="space-y-3">
<div className={containerClass}>
{/* Required fields section */}
{requiredFieldNames.map((fieldName) => (
<FormField
@@ -445,7 +503,7 @@ export function SchemaFormRenderer({
{/* Separator between required and optional */}
{hasRequiredFields && optionalFieldNames.length > 0 && (
<div>
<div className={fullWidthClass}>
<div className="border-t border-border"></div>
</div>
)}
@@ -465,7 +523,7 @@ export function SchemaFormRenderer({
{/* Collapsed optional fields toggle */}
{hasCollapsedFields && (
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
<div className={fullWidthClass}>
<Button
type="button"
variant="ghost"
@@ -1,17 +1,8 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
@@ -20,280 +11,14 @@ import {
DialogClose,
DialogFooter,
} from "@/components/ui/dialog";
import { Send, ChevronDown, ChevronUp } from "lucide-react";
import { Send } from "lucide-react";
import { cn } from "@/lib/utils";
import type { JSONSchemaProperty } from "@/types";
interface FormFieldProps {
name: string;
schema: JSONSchemaProperty;
value: unknown;
onChange: (value: unknown) => void;
isRequired?: boolean;
}
// Helper: Determine if field is likely a short metadata field (not multiline text)
function isShortField(fieldName: string): boolean {
const shortFieldNames = [
"name",
"title",
"id",
"key",
"label",
"type",
"status",
"tag",
"category",
"code",
"username",
"password",
];
return shortFieldNames.includes(fieldName.toLowerCase());
}
function FormField({
name,
schema,
value,
onChange,
isRequired = false,
}: FormFieldProps) {
const { type, description, enum: enumValues, default: defaultValue } = schema;
// Determine if this should be a textarea based on JSON Schema format field
// or heuristics (long descriptions, specific field types)
const shouldBeTextarea =
schema.format === "textarea" || // Explicit format from backend
(description && description.length > 100) || // Long description suggests multiline
(type === "string" && !enumValues && !isShortField(name)); // Default strings to textarea unless they're short metadata fields
// Determine if this field should span full width
const shouldSpanFullWidth =
shouldBeTextarea || (description && description.length > 150);
const shouldSpanTwoColumns =
shouldBeTextarea ||
(description && description.length > 80) ||
type === "array"; // Arrays might need more space for comma-separated values
const fieldContent = (() => {
// Handle different field types based on JSON Schema
switch (type) {
case "string":
if (enumValues) {
// Enum select
return (
<div className="space-y-2">
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
<Select
value={
typeof value === "string" && value
? value
: typeof defaultValue === "string"
? defaultValue
: enumValues[0]
}
onValueChange={(val) => onChange(val)}
>
<SelectTrigger>
<SelectValue placeholder={`Select ${name}`} />
</SelectTrigger>
<SelectContent>
{enumValues.map((option: string) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
} else if (
shouldBeTextarea ||
(description && description.length > 100)
) {
// Multi-line text (including text/message/content fields)
return (
<div className="space-y-2">
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
<Textarea
id={name}
value={typeof value === "string" ? value : ""}
onChange={(e) => onChange(e.target.value)}
placeholder={
typeof defaultValue === "string"
? defaultValue
: `Enter ${name}`
}
rows={shouldBeTextarea ? 4 : 2}
className="min-w-[300px] w-full"
/>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
} else {
// Single-line text
return (
<div className="space-y-2">
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
<Input
id={name}
type="text"
value={typeof value === "string" ? value : ""}
onChange={(e) => onChange(e.target.value)}
placeholder={
typeof defaultValue === "string"
? defaultValue
: `Enter ${name}`
}
/>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
}
case "number":
return (
<div className="space-y-2">
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
<Input
id={name}
type="number"
value={typeof value === "number" ? value : ""}
onChange={(e) => {
const val = parseFloat(e.target.value);
onChange(isNaN(val) ? "" : val);
}}
placeholder={
typeof defaultValue === "number"
? defaultValue.toString()
: `Enter ${name}`
}
/>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
case "boolean":
return (
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id={name}
checked={Boolean(value)}
onCheckedChange={(checked) => onChange(checked)}
/>
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
</div>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
case "array":
return (
<div className="space-y-2">
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
<Textarea
id={name}
value={
Array.isArray(value)
? value.join(", ")
: typeof value === "string"
? value
: ""
}
onChange={(e) => {
const arrayValue = e.target.value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
onChange(arrayValue);
}}
placeholder="Enter items separated by commas"
rows={2}
/>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
case "object":
default:
// For complex objects or unknown types, use JSON textarea
return (
<div className="space-y-2">
<Label htmlFor={name}>
{name}
{isRequired && <span className="text-destructive ml-1">*</span>}
</Label>
<Textarea
id={name}
value={
typeof value === "object" && value !== null
? JSON.stringify(value, null, 2)
: typeof value === "string"
? value
: ""
}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
onChange(parsed);
} catch {
// Keep raw string value if not valid JSON
onChange(e.target.value);
}
}}
placeholder='{"key": "value"}'
rows={3}
/>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
}
})();
// Return the field with appropriate grid column spanning
const getColumnSpan = () => {
if (shouldSpanFullWidth) return "md:col-span-2 lg:col-span-3 xl:col-span-4";
if (shouldSpanTwoColumns) return "xl:col-span-2";
return "";
};
return <div className={getColumnSpan()}>{fieldContent}</div>;
}
import {
SchemaFormRenderer,
filterEmptyOptionalFields,
detectChatMessagePattern,
} from "./schema-form-renderer";
interface WorkflowInputFormProps {
inputSchema: JSONSchemaProperty;
@@ -311,7 +36,6 @@ export function WorkflowInputForm({
className,
}: WorkflowInputFormProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [showAdvancedFields, setShowAdvancedFields] = useState(false);
// Check if we're in embedded mode (being used inside another modal)
const isEmbedded = className?.includes("embedded");
@@ -324,71 +48,27 @@ export function WorkflowInputForm({
const requiredFields = inputSchema.required || [];
const isSimpleInput = inputSchema.type === "string" && !inputSchema.enum;
// Plan D: Separate required and optional fields first
const allOptionalFieldNames = fieldNames.filter(
(name) => !requiredFields.includes(name)
);
// Detect ChatMessage-like pattern for auto-filling role
const isChatMessageLike = detectChatMessagePattern(inputSchema, requiredFields);
// Detect ChatMessage-like pattern
const isChatMessageLike =
requiredFields.includes("role") &&
allOptionalFieldNames.some((f) =>
["text", "message", "content"].includes(f)
) &&
properties["role"]?.type === "string";
// For ChatMessage: hide 'role' field (will be auto-filled)
const requiredFieldNames = fieldNames.filter(
(name) =>
requiredFields.includes(name) && !(isChatMessageLike && name === "role")
);
const optionalFieldNames = allOptionalFieldNames;
// For ChatMessage: prioritize text/message/content field to show first
const sortedOptionalFields = isChatMessageLike
? [...optionalFieldNames].sort((a, b) => {
const priority = (name: string) =>
["text", "message", "content"].includes(name) ? 1 : 0;
return priority(b) - priority(a);
})
: optionalFieldNames;
// Always show ALL required fields + fill to minimum visible with optional fields
// For ChatMessage: show only 1 optional field (text)
const MIN_VISIBLE_FIELDS = isChatMessageLike ? 1 : 6;
const visibleOptionalCount = Math.max(
0,
MIN_VISIBLE_FIELDS - requiredFieldNames.length
);
const visibleOptionalFields = sortedOptionalFields.slice(
0,
visibleOptionalCount
);
const collapsedOptionalFields =
sortedOptionalFields.slice(visibleOptionalCount);
const hasCollapsedFields = collapsedOptionalFields.length > 0;
const hasRequiredFields = requiredFieldNames.length > 0;
// Update canSubmit to check required fields properly
// For ChatMessage: role is auto-filled, so it's always valid
// Validation: check if required fields are filled
const canSubmit = isSimpleInput
? formData.value !== undefined && formData.value !== ""
: requiredFields.length > 0
? requiredFields.every((fieldName) => {
// Auto-filled fields are always valid
if (
isChatMessageLike &&
fieldName === "role" &&
formData["role"] === "user"
) {
return true;
}
return formData[fieldName] !== undefined && formData[fieldName] !== "";
})
: Object.keys(formData).length > 0;
? requiredFields.every((fieldName) => {
// Auto-filled fields are always valid
if (
isChatMessageLike &&
fieldName === "role" &&
formData["role"] === "user"
) {
return true;
}
return formData[fieldName] !== undefined && formData[fieldName] !== "";
})
: Object.keys(formData).length > 0;
// Initialize form data
// Initialize form data with defaults
useEffect(() => {
if (inputSchema.type === "string") {
setFormData({ value: inputSchema.default || "" });
@@ -427,17 +107,7 @@ export function WorkflowInputForm({
onSubmit({ [fieldName]: formData[fieldName] || "" });
} else {
// Filter out empty optional fields before submission
const filteredData: Record<string, unknown> = {};
Object.keys(formData).forEach((key) => {
const value = formData[key];
// Include if: 1) required field, OR 2) has non-empty value
if (
requiredFields.includes(key) ||
(value !== undefined && value !== "" && value !== null)
) {
filteredData[key] = value;
}
});
const filteredData = filterEmptyOptionalFields(inputSchema, formData);
onSubmit(filteredData);
}
} else {
@@ -451,103 +121,50 @@ export function WorkflowInputForm({
setLoading(false);
};
const updateField = (fieldName: string, value: unknown) => {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
const handleFormChange = (newValues: Record<string, unknown>) => {
setFormData(newValues);
};
// Simple string input renderer (for non-object schemas)
const renderSimpleInput = () => (
<div className="space-y-2">
<Label htmlFor="simple-input">Input</Label>
<Textarea
id="simple-input"
value={typeof formData.value === "string" ? formData.value : ""}
onChange={(e) => setFormData({ value: e.target.value })}
placeholder={
typeof inputSchema.default === "string"
? inputSchema.default
: "Enter input"
}
rows={4}
className="min-w-[300px] w-full"
/>
{inputSchema.description && (
<p className="text-sm text-muted-foreground">{inputSchema.description}</p>
)}
</div>
);
// If embedded, just show the form directly
if (isEmbedded) {
return (
<form onSubmit={handleSubmit} className={className}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Simple input */}
{isSimpleInput && (
<FormField
name="Input"
schema={inputSchema}
value={formData.value}
onChange={(value) => updateField("value", value)}
isRequired={false}
/>
)}
{/* Simple input */}
{isSimpleInput && renderSimpleInput()}
{/* Complex form fields - Plan D: Required + Optional separation */}
{!isSimpleInput && (
<>
{/* Required fields section */}
{requiredFieldNames.map((fieldName) => (
<FormField
key={fieldName}
name={fieldName}
schema={properties[fieldName] as JSONSchemaProperty}
value={formData[fieldName]}
onChange={(value) => updateField(fieldName, value)}
isRequired={true}
/>
))}
{/* Separator between required and optional (only if both exist) */}
{hasRequiredFields && optionalFieldNames.length > 0 && (
<div className="sm:col-span-2 border-t border-border my-2"></div>
)}
{/* Visible optional fields */}
{visibleOptionalFields.map((fieldName) => (
<FormField
key={fieldName}
name={fieldName}
schema={properties[fieldName] as JSONSchemaProperty}
value={formData[fieldName]}
onChange={(value) => updateField(fieldName, value)}
isRequired={false}
/>
))}
{/* Collapsed optional fields toggle */}
{hasCollapsedFields && (
<div className="sm:col-span-2">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowAdvancedFields(!showAdvancedFields)}
className="w-full justify-center gap-2"
>
{showAdvancedFields ? (
<>
<ChevronUp className="h-4 w-4" />
Hide {collapsedOptionalFields.length} optional field
{collapsedOptionalFields.length !== 1 ? "s" : ""}
</>
) : (
<>
<ChevronDown className="h-4 w-4" />
Show {collapsedOptionalFields.length} optional field
{collapsedOptionalFields.length !== 1 ? "s" : ""}
</>
)}
</Button>
</div>
)}
{/* Collapsed optional fields - only show when toggled */}
{showAdvancedFields &&
collapsedOptionalFields.map((fieldName) => (
<FormField
key={fieldName}
name={fieldName}
schema={properties[fieldName] as JSONSchemaProperty}
value={formData[fieldName]}
onChange={(value) => updateField(fieldName, value)}
isRequired={false}
/>
))}
</>
)}
</div>
{/* Complex form fields using SchemaFormRenderer */}
{!isSimpleInput && (
<SchemaFormRenderer
schema={inputSchema}
values={formData}
onChange={handleFormChange}
disabled={loading}
hideFields={isChatMessageLike ? ["role"] : []}
layout="grid"
/>
)}
<div className="flex gap-2 mt-4 justify-end">
<Button type="submit" disabled={loading || !canSubmit} size="default">
@@ -628,105 +245,20 @@ export function WorkflowInputForm({
{/* Scrollable Form Content */}
<div className="px-8 py-6 overflow-y-auto flex-1 min-h-0">
<form id="workflow-modal-form" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 md:gap-8 max-w-none">
{/* Simple input */}
{isSimpleInput && (
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
<FormField
name="Input"
schema={inputSchema}
value={formData.value}
onChange={(value) => updateField("value", value)}
isRequired={false}
/>
{inputSchema.description && (
<p className="text-sm text-muted-foreground mt-2">
{inputSchema.description}
</p>
)}
</div>
)}
{/* Simple input */}
{isSimpleInput && renderSimpleInput()}
{/* Complex form fields - Plan D: Required + Optional separation */}
{!isSimpleInput && (
<>
{/* Required fields section */}
{requiredFieldNames.map((fieldName) => (
<FormField
key={fieldName}
name={fieldName}
schema={properties[fieldName] as JSONSchemaProperty}
value={formData[fieldName]}
onChange={(value) => updateField(fieldName, value)}
isRequired={true}
/>
))}
{/* Separator between required and optional (only if both exist) */}
{hasRequiredFields && optionalFieldNames.length > 0 && (
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
<div className="border-t border-border"></div>
</div>
)}
{/* Visible optional fields */}
{visibleOptionalFields.map((fieldName) => (
<FormField
key={fieldName}
name={fieldName}
schema={properties[fieldName] as JSONSchemaProperty}
value={formData[fieldName]}
onChange={(value) => updateField(fieldName, value)}
isRequired={false}
/>
))}
{/* Collapsed optional fields toggle */}
{hasCollapsedFields && (
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() =>
setShowAdvancedFields(!showAdvancedFields)
}
className="w-full justify-center gap-2"
>
{showAdvancedFields ? (
<>
<ChevronUp className="h-4 w-4" />
Hide {collapsedOptionalFields.length} optional
field
{collapsedOptionalFields.length !== 1 ? "s" : ""}
</>
) : (
<>
<ChevronDown className="h-4 w-4" />
Show {collapsedOptionalFields.length} optional
field
{collapsedOptionalFields.length !== 1 ? "s" : ""}
</>
)}
</Button>
</div>
)}
{/* Collapsed optional fields - only show when toggled */}
{showAdvancedFields &&
collapsedOptionalFields.map((fieldName) => (
<FormField
key={fieldName}
name={fieldName}
schema={properties[fieldName] as JSONSchemaProperty}
value={formData[fieldName]}
onChange={(value) => updateField(fieldName, value)}
isRequired={false}
/>
))}
</>
)}
</div>
{/* Complex form fields using SchemaFormRenderer */}
{!isSimpleInput && (
<SchemaFormRenderer
schema={inputSchema}
values={formData}
onChange={handleFormChange}
disabled={loading}
hideFields={isChatMessageLike ? ["role"] : []}
layout="grid"
/>
)}
</form>
</div>
@@ -45,7 +45,14 @@ export interface AgentInfo {
// JSON Schema types for workflow input
export interface JSONSchemaProperty {
type: "string" | "number" | "integer" | "boolean" | "array" | "object";
type?:
| "string"
| "number"
| "integer"
| "boolean"
| "array"
| "object"
| "null";
description?: string;
default?: unknown;
enum?: string[];
@@ -53,6 +60,16 @@ export interface JSONSchemaProperty {
properties?: Record<string, JSONSchemaProperty>;
required?: string[];
items?: JSONSchemaProperty;
// Union types (Pydantic generates these for Optional[T], Union[T1, T2], etc.)
anyOf?: JSONSchemaProperty[];
oneOf?: JSONSchemaProperty[];
allOf?: JSONSchemaProperty[];
// Additional JSON Schema properties
title?: string;
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
}
export interface JSONSchema {