fix(frontend): typed inputs in new run modal (#10799)

## Changes 🏗️

###  Make the **Credentials Inputs** show up on the new Run Agent Modal
<img width="450" height="784" alt="Screenshot 2025-09-02 at 00 54 19"
src="https://github.com/user-attachments/assets/26ad8242-a1bc-45f6-9149-a3d207683679"
/>


### Fixes on other modals...

<img width="450" height="579" alt="Screenshot 2025-09-02 at 00 04 40"
src="https://github.com/user-attachments/assets/fa2f9ed9-207b-4599-9e60-3e37c4be6ea9"
/>
 
<img width="450" height="579" alt="Screenshot 2025-09-02 at 00 04 44"
src="https://github.com/user-attachments/assets/92e062a7-f161-423e-b6c9-f998fbdef102"
/>

<img width="450" height="634" alt="Screenshot 2025-09-02 at 00 47 06"
src="https://github.com/user-attachments/assets/93009809-0df0-44c5-b2d2-a9aa0f501312"
/>


- always use buttons/inputs in small size ( _due to the tight space_ ) 
- use from the design system
- always use pretty scrollbars
- Configure
[`tailwind-scrollbars`](https://github.com/adoxography/tailwind-scrollbar)
for pretty scrollbars
- prevent content in dialog to overflow when scrollable

### 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 with the new agent run modal flag enabled
  - [x] Check the above 

#### For configuration changes:

None
This commit is contained in:
Ubbe
2025-09-03 15:58:07 +09:00
committed by GitHub
parent 4928ce3f90
commit 57ecc10535
23 changed files with 332 additions and 347 deletions

View File

@@ -89,6 +89,7 @@
"shepherd.js": "14.5.1",
"sonner": "2.0.7",
"tailwind-merge": "2.6.0",
"tailwind-scrollbar": "4.0.2",
"tailwindcss-animate": "1.0.7",
"uuid": "11.1.0",
"vaul": "1.1.2",

View File

@@ -200,6 +200,9 @@ importers:
tailwind-merge:
specifier: 2.6.0
version: 2.6.0
tailwind-scrollbar:
specifier: 4.0.2
version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17)
tailwindcss-animate:
specifier: 1.0.7
version: 1.0.7(tailwindcss@3.4.17)
@@ -2930,6 +2933,9 @@ packages:
'@types/phoenix@1.6.6':
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
@@ -5909,6 +5915,11 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
react: '>=16.0.0'
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -6612,6 +6623,12 @@ packages:
tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
tailwind-scrollbar@4.0.2:
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 4.x
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
@@ -10211,6 +10228,8 @@ snapshots:
'@types/phoenix@1.6.6': {}
'@types/prismjs@1.26.5': {}
'@types/prop-types@15.7.15': {}
'@types/react-dom@18.3.5(@types/react@18.3.17)':
@@ -13581,6 +13600,12 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
prism-react-renderer@2.4.1(react@18.3.1):
dependencies:
'@types/prismjs': 1.26.5
clsx: 2.1.1
react: 18.3.1
process-nextick-args@2.0.1: {}
process@0.11.10: {}
@@ -14465,6 +14490,13 @@ snapshots:
tailwind-merge@2.6.0: {}
tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17):
dependencies:
prism-react-renderer: 2.4.1(react@18.3.1)
tailwindcss: 3.4.17
transitivePeerDependencies:
- react
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
dependencies:
tailwindcss: 3.4.17

View File

@@ -1,15 +1,7 @@
import { Input } from "@/components/atoms/Input/Input";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Form, FormDescription, FormField } from "@/components/ui/form";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
@@ -54,6 +46,9 @@ export function APIKeyCredentialsModal({
},
}}
onClose={onClose}
styling={{
maxWidth: "25rem",
}}
>
<Dialog.Content>
{schemaDescription && (
@@ -61,79 +56,65 @@ export function APIKeyCredentialsModal({
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>API Key</FormLabel>
{schema.credentials_scopes && (
<FormDescription>
Required scope(s) for this block:{" "}
{schema.credentials_scopes?.map((s, i, a) => (
<span key={i}>
<code>{s}</code>
{i < a.length - 1 && ", "}
</span>
))}
</FormDescription>
)}
<FormControl>
<Input
id="apiKey"
label="API Key"
hideLabel
type="password"
placeholder="Enter API key..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<>
<Input
id="apiKey"
label="API Key"
type="password"
placeholder="Enter API key..."
size="small"
hint={
schema.credentials_scopes ? (
<FormDescription>
Required scope(s) for this block:{" "}
{schema.credentials_scopes?.map((s, i, a) => (
<span key={i}>
<code className="text-xs font-bold">{s}</code>
{i < a.length - 1 && ", "}
</span>
))}
</FormDescription>
) : null
}
{...field}
/>
</>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
id="title"
label="Name"
hideLabel
type="text"
placeholder="Enter a name for this API key..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<Input
id="title"
label="Name"
type="text"
placeholder="Enter a name for this API key..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="expiresAt"
render={({ field }) => (
<FormItem>
<FormLabel>Expiration Date (Optional)</FormLabel>
<FormControl>
<Input
id="expiresAt"
label="Expiration Date"
hideLabel
type="datetime-local"
placeholder="Select expiration date..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<Input
id="expiresAt"
label="Expiration Date"
type="datetime-local"
placeholder="Select expiration date..."
size="small"
{...field}
/>
)}
/>
<Button type="submit" className="w-full">
<Button type="submit" size="small" className="min-w-68">
Save & use this API key
</Button>
</form>

View File

@@ -335,30 +335,39 @@ export const CredentialsInput: FC<{
// Show credentials creation UI when no relevant credentials exist
if (!hasRelevantCredentials) {
return (
<div>
<div className="mb-4">
{fieldHeader}
<div className={cn("flex flex-row space-x-2", className)}>
{supportsOAuth2 && (
<Button onClick={handleOAuthLogin}>
<Button onClick={handleOAuthLogin} size="small">
<ProviderIcon className="mr-2 h-4 w-4" />
{"Sign in with " + providerName}
</Button>
)}
{supportsApiKey && (
<Button onClick={() => setAPICredentialsModalOpen(true)}>
<Button
onClick={() => setAPICredentialsModalOpen(true)}
size="small"
>
<ProviderIcon className="mr-2 h-4 w-4" />
Enter API key
</Button>
)}
{supportsUserPassword && (
<Button onClick={() => setUserPasswordCredentialsModalOpen(true)}>
<Button
onClick={() => setUserPasswordCredentialsModalOpen(true)}
size="small"
>
<ProviderIcon className="mr-2 h-4 w-4" />
Enter username and password
</Button>
)}
{supportsHostScoped && credentials.discriminatorValue && (
<Button onClick={() => setHostScopedCredentialsModalOpen(true)}>
<Button
onClick={() => setHostScopedCredentialsModalOpen(true)}
size="small"
>
<ProviderIcon className="mr-2 h-4 w-4" />
{`Enter sensitive headers for ${getHostFromUrl(credentials.discriminatorValue)}`}
</Button>

View File

@@ -7,12 +7,9 @@ import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import useCredentials from "@/hooks/useCredentials";
import {
@@ -20,6 +17,7 @@ import {
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { getHostFromUrl } from "@/lib/utils/url";
import { PlusIcon, TrashIcon } from "@phosphor-icons/react";
type Props = {
schema: BlockIOCredentialsSubSchema;
@@ -139,6 +137,9 @@ export function HostScopedCredentialsModal({
},
}}
onClose={onClose}
styling={{
maxWidth: "25rem",
}}
>
<Dialog.Content>
{schema.description && (
@@ -146,81 +147,74 @@ export function HostScopedCredentialsModal({
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host Pattern</FormLabel>
<FormDescription>
{currentHost
<Input
id="host"
label="Host Pattern"
type="text"
size="small"
readOnly={!!currentHost}
hint={
currentHost
? "Auto-populated from the URL field. Headers will be applied to requests to this host."
: "Enter the host/domain to match against request URLs (e.g., api.example.com)."}
</FormDescription>
<FormControl>
<Input
id="host"
label="Host Pattern"
hideLabel
type="text"
readOnly={!!currentHost}
placeholder={
currentHost
? undefined
: "Enter host (e.g., api.example.com)"
}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
: "Enter the host/domain to match against request URLs (e.g., api.example.com)."
}
placeholder={
currentHost
? undefined
: "Enter host (e.g., api.example.com)"
}
{...field}
/>
)}
/>
<div className="space-y-2">
<FormLabel>Headers</FormLabel>
<FormDescription>
<FormDescription className="max-w-md">
Add sensitive headers (like Authorization, X-API-Key) that
should be automatically included in requests to the specified
host.
</FormDescription>
{headerPairs.map((pair, index) => (
<div key={index} className="flex items-end gap-2">
<div className="flex-1">
<Input
id={`header-${index}-key`}
label="Header Name"
hideLabel
placeholder="Header name (e.g., Authorization)"
value={pair.key}
onChange={(e) =>
updateHeaderPair(index, "key", e.target.value)
}
/>
</div>
<div className="flex-1">
<Input
id={`header-${index}-value`}
label="Header Value"
hideLabel
type="password"
placeholder="Header value (e.g., Bearer token123)"
value={pair.value}
onChange={(e) =>
updateHeaderPair(index, "value", e.target.value)
}
/>
</div>
<div key={index} className="flex w-full items-center gap-4">
<Input
id={`header-${index}-key`}
label="Header Name"
placeholder="Header name (e.g., Authorization)"
size="small"
value={pair.key}
className="flex-1"
onChange={(e) =>
updateHeaderPair(index, "key", e.target.value)
}
/>
<Input
id={`header-${index}-value`}
label="Header Value"
size="small"
type="password"
className="flex-2"
placeholder="Header value (e.g., Bearer token123)"
value={pair.value}
onChange={(e) =>
updateHeaderPair(index, "value", e.target.value)
}
/>
<Button
type="button"
variant="outline"
variant="secondary"
size="small"
onClick={() => removeHeaderPair(index)}
disabled={headerPairs.length === 1}
>
Remove
<TrashIcon className="size-4" /> Remove
</Button>
</div>
))}
@@ -230,15 +224,16 @@ export function HostScopedCredentialsModal({
variant="outline"
size="small"
onClick={addHeaderPair}
className="w-full"
>
Add Another Header
<PlusIcon className="size-4" /> Add Another Header
</Button>
</div>
<Button type="submit" className="w-full">
Save & use these credentials
</Button>
<div className="pt-8">
<Button type="submit" className="w-full" size="small">
Save & use these credentials
</Button>
</div>
</form>
</Form>
</Dialog.Content>

View File

@@ -1,22 +1,10 @@
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/atoms/Input/Input";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Form, FormField } from "@/components/ui/form";
import useCredentials from "@/hooks/useCredentials";
import {
BlockIOCredentialsSubSchema,
@@ -81,76 +69,72 @@ export function PasswordCredentialsModal({
return (
<Dialog
open={open}
onOpenChange={(open) => {
if (!open) onClose();
title={`Add new username & password for ${providerName}`}
controlled={{
isOpen: open,
set: (isOpen) => {
if (!isOpen) onClose();
},
}}
onClose={onClose}
styling={{
maxWidth: "25rem",
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
Add new username & password for {providerName}
</DialogTitle>
</DialogHeader>
<Dialog.Content>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-2 pt-4"
>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Enter username..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<Input
id="username"
label="Username"
type="text"
placeholder="Enter username..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter password..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<Input
id="password"
label="Password"
type="password"
placeholder="Enter password..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Enter a name for this user login..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<Input
id="title"
label="Name"
type="text"
placeholder="Enter a name for this user login..."
size="small"
{...field}
/>
)}
/>
<Button type="submit" className="w-full">
<Button type="submit" size="small" className="min-w-68">
Save & use this user login
</Button>
</form>
</Form>
</DialogContent>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -56,6 +56,7 @@ export function RunAgentInputs({
id={`${baseId}-number`}
label={schema.title ?? placeholder ?? "Number"}
hideLabel
size="small"
type="number"
value={value ?? ""}
placeholder={placeholder || "Enter number"}
@@ -73,6 +74,7 @@ export function RunAgentInputs({
id={`${baseId}-textarea`}
label={schema.title ?? placeholder ?? "Text"}
hideLabel
size="small"
type="textarea"
rows={3}
value={value ?? ""}
@@ -105,6 +107,7 @@ export function RunAgentInputs({
id={`${baseId}-date`}
label={schema.title ?? placeholder ?? "Date"}
hideLabel
size="small"
type="date"
value={value ? format(value as Date, "yyyy-MM-dd") : ""}
onChange={(e) => {
@@ -133,6 +136,7 @@ export function RunAgentInputs({
id={`${baseId}-datetime`}
label={schema.title ?? placeholder ?? "Date time"}
hideLabel
size="small"
type="datetime-local"
value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -167,6 +171,7 @@ export function RunAgentInputs({
label={schema.title ?? placeholder ?? "Select"}
hideLabel
value={value ?? ""}
size="small"
onValueChange={(val: string) => onChange(val)}
placeholder={placeholder || "Select an option"}
options={schema.enum
@@ -189,6 +194,7 @@ export function RunAgentInputs({
items={allKeys.map((key) => ({
value: key,
label: _schema.properties[key]?.title ?? key,
size: "small",
}))}
selectedValues={selectedValues}
onChange={(values: string[]) =>
@@ -212,6 +218,7 @@ export function RunAgentInputs({
id={`${baseId}-text`}
label={schema.title ?? placeholder ?? "Text"}
hideLabel
size="small"
type="text"
value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)}

View File

@@ -26,28 +26,45 @@ interface Props {
export function RunAgentModal({ triggerSlot, agent }: Props) {
const {
// UI state
isOpen,
setIsOpen,
showScheduleView,
// Run mode
defaultRunType,
// Form: regular inputs
inputValues,
setInputValues,
// Form: credentials
inputCredentials,
setInputCredentials,
// Preset/trigger labels
presetName,
presetDescription,
setPresetName,
setPresetDescription,
// Scheduling
scheduleName,
cronExpression,
// Validation/readiness
allRequiredInputsAreSet,
// agentInputFields, // Available if needed for future use
// Schemas
agentInputFields,
agentCredentialsInputFields,
hasInputFields,
// Async states
isExecuting,
isCreatingSchedule,
isSettingUpTrigger,
// Actions
handleRun,
handleSchedule,
handleShowSchedule,
@@ -58,6 +75,10 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
const [isScheduleFormValid, setIsScheduleFormValid] = useState(true);
const hasAnySetupFields =
Object.keys(agentInputFields || {}).length > 0 ||
Object.keys(agentCredentialsInputFields || {}).length > 0;
function handleInputChange(key: string, value: string) {
setInputValues((prev) => ({
...prev,
@@ -97,7 +118,7 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
>
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
<Dialog.Content>
<div className="flex h-full flex-col">
<div className="flex h-full flex-col pb-4">
{/* Header */}
<div className="flex-shrink-0">
<ModalHeader agent={agent} />
@@ -105,13 +126,10 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
</div>
{/* Scrollable content */}
<div
className="flex-1 overflow-y-auto overflow-x-hidden pr-1"
style={{ scrollbarGutter: "stable" }}
>
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
{/* Setup Section */}
<div className="mt-10">
{hasInputFields ? (
{hasAnySetupFields ? (
<RunAgentModalContextProvider
value={{
agent,
@@ -191,30 +209,31 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
<AgentDetails agent={agent} />
</div>
</div>
{/* Fixed Actions - sticky inside dialog scroll */}
<Dialog.Footer className="sticky bottom-0 z-10 bg-white">
{showScheduleView ? (
<ScheduleActions
onSchedule={handleSchedule}
isCreatingSchedule={isCreatingSchedule}
allRequiredInputsAreSet={
allRequiredInputsAreSet &&
!!scheduleName.trim() &&
isScheduleFormValid
}
/>
) : (
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
allRequiredInputsAreSet={allRequiredInputsAreSet}
/>
)}
</Dialog.Footer>
</div>
<Dialog.Footer
className="fixed bottom-1 left-0 z-10 w-full bg-white p-4"
style={{ boxShadow: "0px -8px 10px white" }}
>
{showScheduleView ? (
<ScheduleActions
onSchedule={handleSchedule}
isCreatingSchedule={isCreatingSchedule}
allRequiredInputsAreSet={
allRequiredInputsAreSet &&
!!scheduleName.trim() &&
isScheduleFormValid
}
/>
) : (
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
allRequiredInputsAreSet={allRequiredInputsAreSet}
/>
)}
</Dialog.Footer>
</Dialog.Content>
</Dialog>
</>

View File

@@ -1,51 +0,0 @@
import { Input } from "@/components/atoms/Input/Input";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
interface Props {
agent: LibraryAgent;
inputValues: Record<string, any>;
onInputChange: (key: string, value: string) => void;
variant?: "default" | "schedule";
}
export function AgentInputFields({
agent,
inputValues,
onInputChange,
variant = "default",
}: Props) {
const hasInputFields =
agent.input_schema &&
typeof agent.input_schema === "object" &&
"properties" in agent.input_schema;
if (!hasInputFields) {
const emptyStateClass =
variant === "schedule"
? "rounded-lg bg-neutral-50 p-4 text-sm text-neutral-500"
: "p-4 text-sm text-neutral-500";
return (
<div className={emptyStateClass}>
No input fields required for this agent
</div>
);
}
return (
<>
{Object.entries((agent.input_schema as any).properties || {}).map(
([key, schema]: [string, any]) => (
<Input
key={key}
id={key}
label={schema.title || key}
value={inputValues[key] || ""}
onChange={(e) => onInputChange(key, e.target.value)}
placeholder={schema.description}
/>
),
)}
</>
);
}

View File

@@ -36,6 +36,7 @@ export function DefaultRunView() {
<Input
id="trigger_name"
label="Trigger Name"
size="small"
hideLabel
value={presetName}
placeholder="Enter trigger name"
@@ -50,6 +51,7 @@ export function DefaultRunView() {
<Input
id="trigger_description"
label="Trigger Description"
size="small"
hideLabel
value={presetDescription}
placeholder="Enter trigger description"

View File

@@ -326,32 +326,47 @@ export function useAgentRunModal(
}, [agentInputFields]);
return {
// UI state
isOpen,
setIsOpen,
showScheduleView,
// Run mode
defaultRunType,
// Form: regular inputs
inputValues,
setInputValues,
// Form: credentials
inputCredentials,
setInputCredentials,
// Preset/trigger labels
presetName,
presetDescription,
setPresetName,
setPresetDescription,
// Scheduling
scheduleName,
cronExpression,
// Validation/readiness
allRequiredInputsAreSet,
missingInputs,
// Expose credential readiness for any UI hints if needed
// but enforcement is already applied in allRequiredInputsAreSet
// allCredentialsAreSet,
// missingCredentials,
// Schemas for rendering
agentInputFields,
agentCredentialsInputFields,
hasInputFields,
// Async states
isExecuting: executeGraphMutation.isPending,
isCreatingSchedule: createScheduleMutation.isPending,
isSettingUpTrigger: setupTriggerMutation.isPending,
// Actions
handleRun,
handleSchedule,
handleShowSchedule,

View File

@@ -2,8 +2,6 @@
@tailwind components;
@tailwind utilities;
@plugin 'tailwind-scrollbar';
@layer base {
:root {
--background: 0 0% 98%; /* neutral-50#FAFAFA */

View File

@@ -332,13 +332,7 @@ export const CustomNode = React.memo(
return (
<div className="flex flex-col gap-2">
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleSSOLogin}
disabled={isLoading}
>
<Button type="button" onClick={handleSSOLogin} disabled={isLoading}>
{isLoading ? (
"Loading..."
) : (

View File

@@ -178,7 +178,11 @@ export function Input({
<Text variant="body-medium" as="span" className="text-black">
{label}
</Text>
{hint}
{hint ? (
<Text variant="small" as="span" className="!text-zinc-400">
{hint}
</Text>
) : null}
</div>
{inputWithError}
</label>

View File

@@ -6,6 +6,8 @@ import { Button } from "../../../../atoms/Button/Button";
import { StepHeader } from "../StepHeader";
import { Skeleton } from "@/components/ui/skeleton";
import { useAgentSelectStep } from "./useAgentSelectStep";
import { scrollbarStyles } from "@/components/styles/scrollbars";
import { cn } from "@/lib/utils";
interface Props {
onSelect: (agentId: string, agentVersion: number) => void;
@@ -110,7 +112,10 @@ export function AgentSelectStep({
<div className="flex-grow overflow-hidden p-4 sm:p-6">
<h3 className="sr-only">List of agents</h3>
<div
className="h-[300px] overflow-y-auto pr-2 sm:h-[400px] md:h-[500px]"
className={cn(
scrollbarStyles,
"h-[300px] overflow-y-auto pr-2 sm:h-[400px] md:h-[500px]",
)}
role="region"
aria-labelledby="agentListHeading"
>

View File

@@ -4,12 +4,14 @@ interface Props {
children: React.ReactNode;
testId?: string;
className?: string;
style?: React.CSSProperties;
}
export function BaseFooter({
children,
testId = "modal-footer",
className = "",
style,
}: Props) {
const ctx = useDialogCtx();
@@ -17,6 +19,7 @@ export function BaseFooter({
<div
className={`flex justify-end gap-4 pt-6 ${className}`}
data-testid={testId}
style={style}
>
{children}
</div>

View File

@@ -1,11 +1,16 @@
import { cn } from "@/lib/utils";
import { X } from "@phosphor-icons/react";
import * as RXDialog from "@radix-ui/react-dialog";
import { CSSProperties, PropsWithChildren } from "react";
import {
CSSProperties,
PropsWithChildren,
useEffect,
useRef,
useState,
} from "react";
import { DialogCtx } from "../useDialogCtx";
import { modalStyles } from "./styles";
import styles from "./styles.module.css";
import { Button } from "@/components/atoms/Button/Button";
import { scrollbarStyles } from "@/components/styles/scrollbars";
type BaseProps = DialogCtx & PropsWithChildren;
@@ -22,6 +27,25 @@ export function DialogWrap({
isForceOpen,
handleClose,
}: Props) {
const scrollRef = useRef<HTMLDivElement | null>(null);
const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false);
useEffect(() => {
function update() {
const el = scrollRef.current;
if (!el) return;
setHasVerticalScrollbar(el.scrollHeight > el.clientHeight + 1);
}
update();
const ro = new ResizeObserver(update);
if (scrollRef.current) ro.observe(scrollRef.current);
window.addEventListener("resize", update);
return () => {
ro.disconnect();
window.removeEventListener("resize", update);
};
}, []);
return (
<RXDialog.Portal>
<RXDialog.Overlay className={modalStyles.overlay} />
@@ -51,17 +75,23 @@ export function DialogWrap({
)}
{isForceOpen && !handleClose ? null : (
<Button
variant="ghost"
<button
onClick={handleClose}
aria-label="Close"
className="absolute -right-2 top-2 z-50 hover:border-transparent hover:bg-transparent"
className="absolute right-4 top-4 z-50 hover:border-transparent hover:bg-transparent focus:border-none focus:outline-none"
>
<X className={modalStyles.icon} />
</Button>
</button>
)}
</div>
<div className={`overflow-y-auto ${styles.scrollableContent}`}>
<div
ref={scrollRef}
className={cn("overflow-y-auto overflow-x-hidden", scrollbarStyles)}
style={{
scrollbarGutter: "stable",
marginRight: hasVerticalScrollbar ? "-14px" : "0px",
}}
>
{children}
</div>
</RXDialog.Content>

View File

@@ -4,7 +4,6 @@ import { PropsWithChildren } from "react";
import { Drawer } from "vaul";
import { DialogCtx } from "../useDialogCtx";
import { drawerStyles, modalStyles } from "./styles";
import styles from "./styles.module.css";
type BaseProps = DialogCtx & PropsWithChildren;
@@ -62,9 +61,7 @@ export function DrawerWrap({
)
) : null}
</div>
<div className={`overflow-auto ${styles.scrollableContent}`}>
{children}
</div>
<div>{children}</div>
</Drawer.Content>
</Drawer.Portal>
);

View File

@@ -1,43 +0,0 @@
/* Scrollable content area for dialogs */
.scrollableContent {
scrollbar-width: thin;
scrollbar-color: rgb(209, 213, 219) white;
}
/* Webkit scrollbar styles */
.scrollableContent::-webkit-scrollbar {
width: 8px;
}
.scrollableContent::-webkit-scrollbar-track {
background: white;
border-radius: 4px;
}
.scrollableContent::-webkit-scrollbar-thumb {
background: rgb(209, 213, 219); /* gray-300 */
border-radius: 4px;
}
.scrollableContent::-webkit-scrollbar-thumb:hover {
background: rgb(156, 163, 175); /* gray-400 */
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.scrollableContent {
scrollbar-color: rgb(107, 114, 128) rgb(31, 41, 55);
}
.scrollableContent::-webkit-scrollbar-track {
background: rgb(31, 41, 55);
}
.scrollableContent::-webkit-scrollbar-thumb {
background: rgb(107, 114, 128); /* gray-500 */
}
.scrollableContent::-webkit-scrollbar-thumb:hover {
background: rgb(75, 85, 99); /* gray-600 */
}
}

View File

@@ -4,13 +4,13 @@ const commonStyles = {
overlay:
"fixed inset-0 z-50 bg-stone-500/20 dark:bg-black/50 backdrop-blur-md animate-fade-in",
content:
"bg-white p-6 fixed rounded-2xlarge flex flex-col z-50 w-full overflow-hidden",
"overflow-y-hidden bg-white p-6 fixed rounded-2xlarge flex flex-col z-50 w-full",
};
// Modal specific styles
export const modalStyles = {
...commonStyles,
content: `${commonStyles.content} p-6 border border-stone-200 overflow-y-auto min-w-[40vw] max-w-[60vw] max-h-[95vh] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-fadein`,
content: `${commonStyles.content} p-6 border border-stone-200 min-w-[40vw] max-w-[60vw] max-h-[95vh] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-fadein`,
iconWrap:
"absolute top-2 right-3 bg-transparent p-2 rounded-full transition-colors duration-300 ease-in-out outline-none border-none",
icon: "w-4 h-4 text-stone-800",

View File

@@ -0,0 +1,2 @@
export const scrollbarStyles =
"scrollbar-thin scrollbar-thumb-zinc-300 scrollbar-track-transparent";

View File

@@ -256,7 +256,7 @@ const MultiSelectorList = forwardRef<
<CommandList
ref={ref}
className={cn(
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors",
"absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground scrollbar-thumb-rounded-lg dark:scrollbar-thumb-muted",
className,
)}
>

View File

@@ -1,5 +1,6 @@
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import scrollbar from "tailwind-scrollbar";
import { colors } from "./src/components/styles/colors";
const config = {
@@ -157,7 +158,7 @@ const config = {
},
},
},
plugins: [tailwindcssAnimate],
plugins: [tailwindcssAnimate, scrollbar({ nocompatible: true })],
} satisfies Config;
export default config;