fix(frontend+backend): prefill block inputs and hide advanced in CoPilot setup card

Backend:
- get_inputs_from_schema() now accepts input_data to populate each field's
  value with what CoPilot already provided, and includes the advanced flag
  from the schema so the frontend can hide non-essential fields.

Frontend:
- SetupRequirementsCard prefills form inputs from backend-provided values
  instead of showing empty forms
- Advanced fields hidden by default with "Show advanced fields" toggle
  (matching builder behaviour)
- siblingInputs built from both input values and discriminator_values
  so the host pattern modal can extract the host from the URL
- extractInitialValues() populates form state from prefilled values
This commit is contained in:
Zamil Majdy
2026-04-02 13:24:49 +02:00
parent 4e9169c1a2
commit 1364616ff1
3 changed files with 106 additions and 52 deletions

View File

@@ -48,27 +48,41 @@ logger = logging.getLogger(__name__)
def get_inputs_from_schema(
input_schema: dict[str, Any],
exclude_fields: set[str] | None = None,
input_data: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Extract input field info from JSON schema."""
"""Extract input field info from JSON schema.
When *input_data* is provided, each field's ``value`` key is populated
with the value the CoPilot already supplied — so the frontend can
prefill the form instead of showing empty inputs. Fields marked
``advanced`` in the schema are flagged so the frontend can hide them
by default (matching the builder behaviour).
"""
if not isinstance(input_schema, dict):
return []
exclude = exclude_fields or set()
properties = input_schema.get("properties", {})
required = set(input_schema.get("required", []))
provided = input_data or {}
return [
{
results: list[dict[str, Any]] = []
for name, schema in properties.items():
if name in exclude:
continue
entry: dict[str, Any] = {
"name": name,
"title": schema.get("title", name),
"type": schema.get("type", "string"),
"description": schema.get("description", ""),
"required": name in required,
"default": schema.get("default"),
"advanced": schema.get("advanced", False),
}
for name, schema in properties.items()
if name not in exclude
]
if name in provided:
entry["value"] = provided[name]
results.append(entry)
return results
async def execute_block(
@@ -446,7 +460,9 @@ async def prepare_block_for_execution(
requirements={
"credentials": missing_creds_list,
"inputs": get_inputs_from_schema(
input_schema, exclude_fields=credentials_fields
input_schema,
exclude_fields=credentials_fields,
input_data=input_data,
),
"execution_modes": ["immediate"],
},

View File

@@ -6,7 +6,7 @@ import { Text } from "@/components/atoms/Text/Text";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
import {
@@ -14,18 +14,13 @@ import {
buildSiblingInputsFromCredentials,
coerceCredentialFields,
coerceExpectedInputs,
extractInitialValues,
} from "./helpers";
interface Props {
output: SetupRequirementsResponse;
/** Override the message sent to the chat when the user clicks Proceed after connecting credentials.
* Defaults to "Please re-run this step now." */
retryInstruction?: string;
/** Override the label shown above the credentials section.
* Defaults to "Credentials". */
credentialsLabel?: string;
/** Called after Proceed is clicked so the parent can persist the dismissed state
* across remounts (avoids re-enabling the Proceed button on remount). */
onComplete?: () => void;
}
@@ -40,22 +35,38 @@ export function SetupRequirementsCard({
const [inputCredentials, setInputCredentials] = useState<
Record<string, CredentialsMetaInput | undefined>
>({});
const [inputValues, setInputValues] = useState<Record<string, unknown>>({});
const [hasSent, setHasSent] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
const { credentialFields, requiredCredentials } = coerceCredentialFields(
output.setup_info.user_readiness?.missing_credentials,
);
const siblingInputs = buildSiblingInputsFromCredentials(
output.setup_info.user_readiness?.missing_credentials,
);
const expectedInputs = coerceExpectedInputs(
(output.setup_info.requirements as Record<string, unknown>)?.inputs,
);
const inputSchema = buildExpectedInputsSchema(expectedInputs);
const initialValues = useMemo(
() => extractInitialValues(expectedInputs),
[expectedInputs],
);
const [inputValues, setInputValues] =
useState<Record<string, unknown>>(initialValues);
const hasAdvancedFields = expectedInputs.some((i) => i.advanced);
const inputSchema = buildExpectedInputsSchema(expectedInputs, showAdvanced);
// Build siblingInputs for credential modal host prefill.
// Prefer discriminator_values from the credential response, but also
// include values from input_data (e.g. url field) so the host pattern
// can be extracted even when discriminator_values is empty.
const siblingInputs = useMemo(() => {
const fromCreds = buildSiblingInputsFromCredentials(
output.setup_info.user_readiness?.missing_credentials,
);
return { ...inputValues, ...fromCreds };
}, [output.setup_info.user_readiness?.missing_credentials, inputValues]);
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
setInputCredentials((prev) => ({ ...prev, [key]: value }));
@@ -68,10 +79,10 @@ export function SetupRequirementsCard({
const needsInputs = inputSchema !== null;
const requiredInputNames = expectedInputs
.filter((i) => i.required)
.filter((i) => i.required && !i.advanced)
.map((i) => i.name);
const isAllInputsComplete =
needsInputs &&
!needsInputs ||
requiredInputNames.every((name) => {
const v = inputValues[name];
return v !== undefined && v !== null && v !== "";
@@ -82,8 +93,7 @@ export function SetupRequirementsCard({
}
const canRun =
(!needsCredentials || isAllCredentialsComplete) &&
(!needsInputs || isAllInputsComplete);
(!needsCredentials || isAllCredentialsComplete) && isAllInputsComplete;
function handleRun() {
setHasSent(true);
@@ -138,7 +148,9 @@ export function SetupRequirementsCard({
<FormRenderer
jsonSchema={inputSchema}
className="mb-3 mt-3"
handleChange={(v) => setInputValues(v.formData ?? {})}
handleChange={(v) =>
setInputValues((prev) => ({ ...prev, ...(v.formData ?? {}) }))
}
uiSchema={{
"ui:submitButtonOptions": { norender: true },
}}
@@ -148,6 +160,15 @@ export function SetupRequirementsCard({
size: "small",
}}
/>
{hasAdvancedFields && (
<button
type="button"
className="text-xs text-muted-foreground underline"
onClick={() => setShowAdvanced((v) => !v)}
>
{showAdvanced ? "Hide advanced fields" : "Show advanced fields"}
</button>
)}
</div>
)}

View File

@@ -110,21 +110,19 @@ export function buildSiblingInputsFromCredentials(
return result;
}
export function coerceExpectedInputs(rawInputs: unknown): Array<{
interface ExpectedInput {
name: string;
title: string;
type: string;
description?: string;
required: boolean;
}> {
advanced: boolean;
value?: unknown;
}
export function coerceExpectedInputs(rawInputs: unknown): ExpectedInput[] {
if (!Array.isArray(rawInputs)) return [];
const results: Array<{
name: string;
title: string;
type: string;
description?: string;
required: boolean;
}> = [];
const results: ExpectedInput[] = [];
rawInputs.forEach((value, index) => {
if (!value || typeof value !== "object") return;
@@ -144,15 +142,13 @@ export function coerceExpectedInputs(rawInputs: unknown): Array<{
? input.description.trim()
: undefined;
const required = Boolean(input.required);
const advanced = Boolean(input.advanced);
const item: {
name: string;
title: string;
type: string;
description?: string;
required: boolean;
} = { name, title, type, required };
const item: ExpectedInput = { name, title, type, required, advanced };
if (description) item.description = description;
if (input.value !== undefined && input.value !== null) {
item.value = input.value;
}
results.push(item);
});
@@ -162,17 +158,20 @@ export function coerceExpectedInputs(rawInputs: unknown): Array<{
/**
* Build an RJSF schema from expected inputs so they can be rendered
* as a dynamic form via FormRenderer.
*
* When ``showAdvanced`` is false (default), fields marked ``advanced``
* are excluded — matching the builder behaviour where advanced fields
* are hidden behind a toggle.
*/
export function buildExpectedInputsSchema(
expectedInputs: Array<{
name: string;
title: string;
type: string;
description?: string;
required: boolean;
}>,
expectedInputs: ExpectedInput[],
showAdvanced = false,
): RJSFSchema | null {
if (expectedInputs.length === 0) return null;
const visible = showAdvanced
? expectedInputs
: expectedInputs.filter((i) => !i.advanced);
if (visible.length === 0) return null;
const TYPE_MAP: Record<string, string> = {
string: "string",
@@ -189,12 +188,14 @@ export function buildExpectedInputsSchema(
const properties: Record<string, Record<string, unknown>> = {};
const required: string[] = [];
for (const input of expectedInputs) {
properties[input.name] = {
for (const input of visible) {
const prop: Record<string, unknown> = {
type: TYPE_MAP[input.type.toLowerCase()] ?? "string",
title: input.title,
...(input.description ? { description: input.description } : {}),
};
if (input.description) prop.description = input.description;
if (input.value !== undefined) prop.default = input.value;
properties[input.name] = prop;
if (input.required) required.push(input.name);
}
@@ -204,3 +205,19 @@ export function buildExpectedInputsSchema(
...(required.length > 0 ? { required } : {}),
};
}
/**
* Extract initial form values from expected inputs that have a
* prefilled ``value`` from the backend.
*/
export function extractInitialValues(
expectedInputs: ExpectedInput[],
): Record<string, unknown> {
const values: Record<string, unknown> = {};
for (const input of expectedInputs) {
if (input.value !== undefined && input.value !== null) {
values[input.name] = input.value;
}
}
return values;
}