mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
committed by
GitHub
Unverified
parent
e0b9be7e08
commit
0f29637b86
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";
|
||||
|
||||
+72
-14
@@ -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"
|
||||
|
||||
+73
-541
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user