feat(frontend): imporve agent inputs read-only (#11621)

## Changes 🏗️

The main goal of this PR is to improve how we display inputs used for a
given task.

Agent inputs can be of many types (text, long text, date, select, file,
etc.). Until now, we have tried to display them as text, which has not
always worked. Given we already have `<RunAgentInputs />`, which uses
form elements to display the inputs ( _prefilled with data_ ), most of
the time it will look better and less buggy than text.

### Before

<img width="800" height="614" alt="Screenshot 2025-12-14 at 17 45 44"
src="https://github.com/user-attachments/assets/3d851adf-9638-46c1-adfa-b5e68dc78bb0"
/>

### After

<img width="800" height="708" alt="Screenshot 2025-12-14 at 17 45 21"
src="https://github.com/user-attachments/assets/367f32b4-2c30-4368-8d63-4cad06e32437"
/>

### Other improvements

- 🗑️  Removed `<EditInputsModal />`
- it is not used given the API does not support editing inputs for a
schedule yt
- Made `<InformationTooltip />` icon size customisable    

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally
- [x] Check the new view tasks use the form elements instead of text to
display inputs
This commit is contained in:
Ubbe
2025-12-15 00:11:27 +07:00
committed by GitHub
parent aefac541d9
commit 7f7ef6a271
10 changed files with 94 additions and 297 deletions

View File

@@ -1,16 +1,10 @@
"use client"; "use client";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
BlockIOSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs"; import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
import { import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
getAgentCredentialsFields, import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
getAgentInputFields,
renderValue,
} from "./helpers";
type Props = { type Props = {
agent: LibraryAgent; agent: LibraryAgent;
@@ -28,13 +22,13 @@ export function AgentInputsReadOnly({
getAgentCredentialsFields(agent), getAgentCredentialsFields(agent),
); );
// Take actual input entries as leading; augment with schema from input fields.
// TODO: ensure consistent ordering.
const inputEntries = const inputEntries =
inputs && inputs &&
Object.entries(inputs).map<[string, [BlockIOSubSchema | undefined, any]]>( Object.entries(inputs).map(([key, value]) => ({
([k, v]) => [k, [inputFields[k], v]], key,
); schema: inputFields[key],
value,
}));
const hasInputs = inputEntries && inputEntries.length > 0; const hasInputs = inputEntries && inputEntries.length > 0;
const hasCredentials = credentialInputs && credentialFieldEntries.length > 0; const hasCredentials = credentialInputs && credentialFieldEntries.length > 0;
@@ -48,16 +42,20 @@ export function AgentInputsReadOnly({
{/* Regular inputs */} {/* Regular inputs */}
{hasInputs && ( {hasInputs && (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{inputEntries.map(([key, [schema, value]]) => ( {inputEntries.map(({ key, schema, value }) => {
<div key={key} className="flex flex-col gap-1.5"> if (!schema) return null;
<label className="text-sm font-medium">
{schema?.title || key} return (
</label> <RunAgentInputs
<p className="whitespace-pre-wrap break-words text-sm text-neutral-700"> key={key}
{renderValue(value)} schema={schema}
</p> value={value}
</div> placeholder={schema.description}
))} onChange={() => {}}
readOnly={true}
/>
);
})}
</div> </div>
)} )}

View File

@@ -9,6 +9,7 @@ import { Button } from "@/components/atoms/Button/Button";
import { FileInput } from "@/components/atoms/FileInput/FileInput"; import { FileInput } from "@/components/atoms/FileInput/FileInput";
import { Switch } from "@/components/atoms/Switch/Switch"; import { Switch } from "@/components/atoms/Switch/Switch";
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput"; import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { TimePicker } from "@/components/molecules/TimePicker/TimePicker"; import { TimePicker } from "@/components/molecules/TimePicker/TimePicker";
import { import {
BlockIOObjectSubSchema, BlockIOObjectSubSchema,
@@ -32,6 +33,7 @@ interface Props {
value?: any; value?: any;
placeholder?: string; placeholder?: string;
onChange: (value: any) => void; onChange: (value: any) => void;
readOnly?: boolean;
} }
/** /**
@@ -44,6 +46,7 @@ export function RunAgentInputs({
value, value,
placeholder, placeholder,
onChange, onChange,
readOnly = false,
...props ...props
}: Props & React.HTMLAttributes<HTMLElement>) { }: Props & React.HTMLAttributes<HTMLElement>) {
const { handleUploadFile, uploadProgress } = useRunAgentInputs(); const { handleUploadFile, uploadProgress } = useRunAgentInputs();
@@ -62,7 +65,6 @@ export function RunAgentInputs({
id={`${baseId}-number`} id={`${baseId}-number`}
label={schema.title ?? placeholder ?? "Number"} label={schema.title ?? placeholder ?? "Number"}
hideLabel hideLabel
size="small"
type="number" type="number"
value={value ?? ""} value={value ?? ""}
placeholder={placeholder || "Enter number"} placeholder={placeholder || "Enter number"}
@@ -80,7 +82,6 @@ export function RunAgentInputs({
id={`${baseId}-textarea`} id={`${baseId}-textarea`}
label={schema.title ?? placeholder ?? "Text"} label={schema.title ?? placeholder ?? "Text"}
hideLabel hideLabel
size="small"
type="textarea" type="textarea"
rows={3} rows={3}
value={value ?? ""} value={value ?? ""}
@@ -130,7 +131,6 @@ export function RunAgentInputs({
id={`${baseId}-date`} id={`${baseId}-date`}
label={schema.title ?? placeholder ?? "Date"} label={schema.title ?? placeholder ?? "Date"}
hideLabel hideLabel
size="small"
type="date" type="date"
value={value ? format(value as Date, "yyyy-MM-dd") : ""} value={value ? format(value as Date, "yyyy-MM-dd") : ""}
onChange={(e) => { onChange={(e) => {
@@ -159,7 +159,6 @@ export function RunAgentInputs({
id={`${baseId}-datetime`} id={`${baseId}-datetime`}
label={schema.title ?? placeholder ?? "Date time"} label={schema.title ?? placeholder ?? "Date time"}
hideLabel hideLabel
size="small"
type="datetime-local" type="datetime-local"
value={value ?? ""} value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)} onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -194,7 +193,6 @@ export function RunAgentInputs({
label={schema.title ?? placeholder ?? "Select"} label={schema.title ?? placeholder ?? "Select"}
hideLabel hideLabel
value={value ?? ""} value={value ?? ""}
size="small"
onValueChange={(val: string) => onChange(val)} onValueChange={(val: string) => onChange(val)}
placeholder={placeholder || "Select an option"} placeholder={placeholder || "Select an option"}
options={schema.enum options={schema.enum
@@ -217,7 +215,6 @@ export function RunAgentInputs({
items={allKeys.map((key) => ({ items={allKeys.map((key) => ({
value: key, value: key,
label: _schema.properties[key]?.title ?? key, label: _schema.properties[key]?.title ?? key,
size: "small",
}))} }))}
selectedValues={selectedValues} selectedValues={selectedValues}
onChange={(values: string[]) => onChange={(values: string[]) =>
@@ -336,7 +333,6 @@ export function RunAgentInputs({
id={`${baseId}-text`} id={`${baseId}-text`}
label={schema.title ?? placeholder ?? "Text"} label={schema.title ?? placeholder ?? "Text"}
hideLabel hideLabel
size="small"
type="text" type="text"
value={value ?? ""} value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)} onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -347,6 +343,17 @@ export function RunAgentInputs({
} }
return ( return (
<div className="no-drag relative flex w-full">{innerInputElement}</div> <div className="flex w-full flex-col gap-0 space-y-2">
<label className="large-medium flex items-center gap-1 font-medium">
{schema.title || placeholder}
<InformationTooltip description={schema.description} />
</label>
<div
className="no-drag relative flex w-full"
style={readOnly ? { pointerEvents: "none", opacity: 0.7 } : undefined}
>
{innerInputElement}
</div>
</div>
); );
} }

View File

@@ -73,22 +73,15 @@ export function ModalRunSection() {
title="Task Inputs" title="Task Inputs"
subtitle="Enter the information you want to provide to the agent for this task" subtitle="Enter the information you want to provide to the agent for this task"
> >
{/* Regular inputs */}
{inputFields.map(([key, inputSubSchema]) => ( {inputFields.map(([key, inputSubSchema]) => (
<div key={key} className="flex w-full flex-col gap-0 space-y-2"> <RunAgentInputs
<label className="flex items-center gap-1 text-sm font-medium"> key={key}
{inputSubSchema.title || key} schema={inputSubSchema}
<InformationTooltip description={inputSubSchema.description} /> value={inputValues[key] ?? inputSubSchema.default}
</label> placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
<RunAgentInputs data-testid={`agent-input-${key}`}
schema={inputSubSchema} />
value={inputValues[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
data-testid={`agent-input-${key}`}
/>
</div>
))} ))}
</ModalSection> </ModalSection>
) : null} ) : null}

View File

@@ -4,17 +4,11 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text"; import { Text } from "@/components/atoms/Text/Text";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList"; import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews"; import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { InfoIcon } from "@phosphor-icons/react";
import { useEffect } from "react"; import { useEffect } from "react";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly"; import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { AnchorLinksWrap } from "../AnchorLinksWrap"; import { AnchorLinksWrap } from "../AnchorLinksWrap";
@@ -149,25 +143,12 @@ export function SelectedRunView({
<div id="summary" className="scroll-mt-4"> <div id="summary" className="scroll-mt-4">
<RunDetailCard <RunDetailCard
title={ title={
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<Text variant="lead-semibold">Summary</Text> <Text variant="lead-semibold">Summary</Text>
<TooltipProvider> <InformationTooltip
<Tooltip> iconSize={20}
<TooltipTrigger asChild> description="This AI-generated summary describes how the agent handled your task. It's an experimental feature and may occasionally be inaccurate."
<InfoIcon />
size={16}
className="cursor-help text-neutral-500 hover:text-neutral-700"
/>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
This AI-generated summary describes how the agent
handled your task. It&apos;s an experimental
feature and may occasionally be inaccurate.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
} }
> >
@@ -195,7 +176,17 @@ export function SelectedRunView({
{/* Input Section */} {/* Input Section */}
<div id="input" className="scroll-mt-4"> <div id="input" className="scroll-mt-4">
<RunDetailCard title="Your input"> <RunDetailCard
title={
<div className="flex items-center gap-1">
<Text variant="lead-semibold">Your input</Text>
<InformationTooltip
iconSize={20}
description="This is the input that was provided to the agent for running this task."
/>
</div>
}
>
<AgentInputsReadOnly <AgentInputsReadOnly
agent={agent} agent={agent}
inputs={run?.inputs} inputs={run?.inputs}

View File

@@ -1,84 +0,0 @@
"use client";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { PencilSimpleIcon } from "@phosphor-icons/react";
import { RunAgentInputs } from "../../../../modals/RunAgentInputs/RunAgentInputs";
import { useEditInputsModal } from "./useEditInputsModal";
type Props = {
agent: LibraryAgent;
schedule: GraphExecutionJobInfo;
};
export function EditInputsModal({ agent, schedule }: Props) {
const {
isOpen,
setIsOpen,
inputFields,
values,
setValues,
handleSave,
isSaving,
} = useEditInputsModal(agent, schedule);
return (
<Dialog
controlled={{ isOpen, set: setIsOpen }}
styling={{ maxWidth: "32rem" }}
>
<Dialog.Trigger>
<Button
variant="ghost"
size="small"
className="absolute -right-2 -top-2"
>
<PencilSimpleIcon className="size-4" /> Edit inputs
</Button>
</Dialog.Trigger>
<Dialog.Content>
<div className="flex flex-col gap-4">
<Text variant="h3">Edit inputs</Text>
<div className="flex flex-col gap-4">
{Object.entries(inputFields).map(([key, fieldSchema]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">
{fieldSchema?.title || key}
</label>
<RunAgentInputs
schema={fieldSchema as any}
value={values[key]}
onChange={(v) => setValues((prev) => ({ ...prev, [key]: v }))}
/>
</div>
))}
</div>
</div>
<Dialog.Footer>
<div className="flex w-full justify-end gap-2">
<Button
variant="secondary"
size="small"
onClick={() => setIsOpen(false)}
className="min-w-32"
>
Cancel
</Button>
<Button
variant="primary"
size="small"
onClick={handleSave}
loading={isSaving}
className="min-w-32"
>
{isSaving ? "Saving…" : "Save"}
</Button>
</div>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,78 +0,0 @@
"use client";
import { useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getGetV1ListExecutionSchedulesForAGraphQueryKey } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { useToast } from "@/components/molecules/Toast/use-toast";
function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const schema = agent.input_schema as unknown as {
properties?: Record<string, any>;
} | null;
if (!schema || !schema.properties) return {};
const properties = schema.properties as Record<string, any>;
const visibleEntries = Object.entries(properties).filter(
([, sub]) => !sub?.hidden,
);
return Object.fromEntries(visibleEntries);
}
export function useEditInputsModal(
agent: LibraryAgent,
schedule: GraphExecutionJobInfo,
) {
const queryClient = useQueryClient();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const inputFields = useMemo(() => getAgentInputFields(agent), [agent]);
const [values, setValues] = useState<Record<string, any>>({
...(schedule.input_data as Record<string, any>),
});
async function handleSave() {
setIsSaving(true);
try {
const res = await fetch(`/api/schedules/${schedule.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ inputs: values }),
});
if (!res.ok) {
let message = "Failed to update schedule inputs";
const data = await res.json();
message = data?.message || data?.detail || message;
throw new Error(message);
}
await queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
schedule.graph_id,
),
});
toast({
title: "Schedule inputs updated",
});
setIsOpen(false);
} catch (error: any) {
toast({
title: "Failed to update schedule inputs",
description: error?.message || "An unexpected error occurred.",
variant: "destructive",
});
}
setIsSaving(false);
}
return {
isOpen,
setIsOpen,
inputFields,
values,
setValues,
handleSave,
isSaving,
} as const;
}

View File

@@ -4,7 +4,6 @@ import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExe
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Input } from "@/components/atoms/Input/Input"; import { Input } from "@/components/atoms/Input/Input";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { import {
getAgentCredentialsFields, getAgentCredentialsFields,
getAgentInputFields, getAgentInputFields,
@@ -138,25 +137,13 @@ export function SelectedTemplateView({
<RunDetailCard title="Your Input"> <RunDetailCard title="Your Input">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{inputFields.map(([key, inputSubSchema]) => ( {inputFields.map(([key, inputSubSchema]) => (
<div <RunAgentInputs
key={key} key={key}
className="flex w-full flex-col gap-0 space-y-2" schema={inputSubSchema}
> value={inputs[key] ?? inputSubSchema.default}
<label className="flex items-center gap-1 text-sm font-medium"> placeholder={inputSubSchema.description}
{inputSubSchema.title || key} onChange={(value) => setInputValue(key, value)}
{inputSubSchema.description && ( />
<InformationTooltip
description={inputSubSchema.description}
/>
)}
</label>
<RunAgentInputs
schema={inputSubSchema}
value={inputs[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
/>
</div>
))} ))}
</div> </div>
</RunDetailCard> </RunDetailCard>

View File

@@ -3,7 +3,6 @@
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Input } from "@/components/atoms/Input/Input"; import { Input } from "@/components/atoms/Input/Input";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { import {
getAgentCredentialsFields, getAgentCredentialsFields,
getAgentInputFields, getAgentInputFields,
@@ -131,25 +130,13 @@ export function SelectedTriggerView({
<RunDetailCard title="Your Input"> <RunDetailCard title="Your Input">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{inputFields.map(([key, inputSubSchema]) => ( {inputFields.map(([key, inputSubSchema]) => (
<div <RunAgentInputs
key={key} key={key}
className="flex w-full flex-col gap-0 space-y-2" schema={inputSubSchema}
> value={inputs[key] ?? inputSubSchema.default}
<label className="flex items-center gap-1 text-sm font-medium"> placeholder={inputSubSchema.description}
{inputSubSchema.title || key} onChange={(value) => setInputValue(key, value)}
{inputSubSchema.description && ( />
<InformationTooltip
description={inputSubSchema.description}
/>
)}
</label>
<RunAgentInputs
schema={inputSubSchema}
value={inputs[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
/>
</div>
))} ))}
</div> </div>
</RunDetailCard> </RunDetailCard>

View File

@@ -680,28 +680,20 @@ export function AgentRunDraftView({
{/* Regular inputs */} {/* Regular inputs */}
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => ( {Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
<div key={key} className="flex flex-col space-y-2"> <RunAgentInputs
<label className="flex items-center gap-1 text-sm font-medium"> key={key}
{inputSubSchema.title || key} schema={inputSubSchema}
<InformationTooltip value={inputValues[key] ?? inputSubSchema.default}
description={inputSubSchema.description} placeholder={inputSubSchema.description}
/> onChange={(value) => {
</label> setInputValues((obj) => ({
...obj,
<RunAgentInputs [key]: value,
schema={inputSubSchema} }));
value={inputValues[key] ?? inputSubSchema.default} setChangedPresetAttributes((prev) => prev.add("inputs"));
placeholder={inputSubSchema.description} }}
onChange={(value) => { data-testid={`agent-input-${key}`}
setInputValues((obj) => ({ />
...obj,
[key]: value,
}));
setChangedPresetAttributes((prev) => prev.add("inputs"));
}}
data-testid={`agent-input-${key}`}
/>
</div>
))} ))}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -9,16 +9,20 @@ import ReactMarkdown from "react-markdown";
type Props = { type Props = {
description?: string; description?: string;
iconSize?: number;
}; };
export function InformationTooltip({ description }: Props) { export function InformationTooltip({ description, iconSize = 24 }: Props) {
if (!description) return null; if (!description) return null;
return ( return (
<TooltipProvider delayDuration={400}> <TooltipProvider delayDuration={400}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Info className="rounded-full p-1 hover:bg-slate-50" size={24} /> <Info
className="rounded-full p-1 hover:bg-slate-50"
size={iconSize}
/>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<ReactMarkdown <ReactMarkdown