From 375d33cca9db0fca02db7f52b14d10b8e5ef9668 Mon Sep 17 00:00:00 2001 From: Ubbe Date: Thu, 15 Jan 2026 17:44:44 +0700 Subject: [PATCH] fix(frontend): agent credentials improvements (#11763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes πŸ—οΈ ### System credentials in Run Modal We had the issue that "system" credentials were mixed with "user" credentials in the run agent modal: #### Before Screenshot 2026-01-14 at 19 05 56 This created confusion among the users. This "system" credentials are supplied by AutoGPT ( _most of the time_ ) and a user running an agent should not bother with them ( _unless they want to change them_ ). For example in this case, the credential that matters is the **Google** one πŸ™‡πŸ½ ### After Screenshot 2026-01-14 at 19 04 12 Screenshot 2026-01-14 at 19 04 19 "System" credentials are collapsed by default, reducing noise in the Task Credentials section. The user can still see and change them by expanding the accordion. Screenshot 2026-01-14 at 19 04 27 If some "system" credentials are missing, there is a red label indicating so, it wasn't that obvious with the previous implementation, Screenshot 2026-01-14 at 19 04 30 ### New endpoint There is a new REST endpoint, `GET /providers/system`, to list system credential providers so it is easy to access in the Front-end to group them together vs user ones. ### Other improvements #### `` refinements Screenshot 2026-01-14 at 19 09 31 Use a normal browser ` + )} + /> @@ -90,20 +105,7 @@ export function APIKeyCredentialsModal({ )} /> - ( - - )} - /> + { + const value = e.target.value; + if (value) { + const dateTime = new Date(value); + dateTime.setHours(0, 0, 0, 0); + const year = dateTime.getFullYear(); + const month = String(dateTime.getMonth() + 1).padStart( + 2, + "0", + ); + const day = String(dateTime.getDate()).padStart(2, "0"); + const normalizedValue = `${year}-${month}-${day}T00:00`; + field.onChange(normalizedValue); + } else { + field.onChange(value); + } + }} + onBlur={field.onBlur} + name={field.name} /> )} /> - diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/APIKeyCredentialsModal/useAPIKeyCredentialsModal.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/APIKeyCredentialsModal/useAPIKeyCredentialsModal.ts index 391633bed5..72599a2e79 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/APIKeyCredentialsModal/useAPIKeyCredentialsModal.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/APIKeyCredentialsModal/useAPIKeyCredentialsModal.ts @@ -1,11 +1,11 @@ -import { z } from "zod"; -import { useForm, type UseFormReturn } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; import useCredentials from "@/hooks/useCredentials"; import { BlockIOCredentialsSubSchema, CredentialsMetaInput, } from "@/lib/autogpt-server-api/types"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm, type UseFormReturn } from "react-hook-form"; +import { z } from "zod"; export type APIKeyFormValues = { apiKey: string; @@ -40,12 +40,24 @@ export function useAPIKeyCredentialsModal({ expiresAt: z.string().optional(), }); + function getDefaultExpirationDate(): string { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + const year = tomorrow.getFullYear(); + const month = String(tomorrow.getMonth() + 1).padStart(2, "0"); + const day = String(tomorrow.getDate()).padStart(2, "0"); + const hours = String(tomorrow.getHours()).padStart(2, "0"); + const minutes = String(tomorrow.getMinutes()).padStart(2, "0"); + return `${year}-${month}-${day}T${hours}:${minutes}`; + } + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { apiKey: "", title: "", - expiresAt: "", + expiresAt: getDefaultExpirationDate(), }, }); diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialRow/CredentialRow.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialRow/CredentialRow.tsx index 2d0358aacb..dc69c34d93 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialRow/CredentialRow.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialRow/CredentialRow.tsx @@ -7,7 +7,8 @@ import { DropdownMenuTrigger, } from "@/components/molecules/DropdownMenu/DropdownMenu"; import { cn } from "@/lib/utils"; -import { CaretDown, DotsThreeVertical } from "@phosphor-icons/react"; +import { CaretDownIcon, DotsThreeVertical } from "@phosphor-icons/react"; +import { useEffect, useRef, useState } from "react"; import { fallbackIcon, getCredentialDisplayName, @@ -26,7 +27,7 @@ type CredentialRowProps = { provider: string; displayName: string; onSelect: () => void; - onDelete: () => void; + onDelete?: () => void; readOnly?: boolean; showCaret?: boolean; asSelectTrigger?: boolean; @@ -47,11 +48,32 @@ export function CredentialRow({ }: CredentialRowProps) { const ProviderIcon = providerIcons[provider] || fallbackIcon; const isNodeVariant = variant === "node"; + const containerRef = useRef(null); + const [showMaskedKey, setShowMaskedKey] = useState(true); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const width = entry.contentRect.width; + setShowMaskedKey(width >= 360); + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); return (
{getCredentialDisplayName(credential, displayName)} - {!(asSelectTrigger && isNodeVariant) && ( + {!(asSelectTrigger && isNodeVariant) && showMaskedKey && ( {"*".repeat(MASKED_KEY_LENGTH)} )}
- {showCaret && !asSelectTrigger && ( - + {(showCaret || (asSelectTrigger && !readOnly)) && ( + )} - {!readOnly && !showCaret && !asSelectTrigger && ( + {!readOnly && !showCaret && !asSelectTrigger && onDelete && ( + + )} + + {hasSystemCredentials && ( + + + +
+ System credentials +
+
+ +
+ {showTitle && ( +
+ + {displayName} credentials + {isOptional && ( + + (optional) + + )} + + {schema.description && ( + + )} +
+ )} + {credentialsInAccordion.length > 0 && ( + + )} + {isSystemProvider && ( + + )} +
+
+
+
+ )} + + {!showUserCredentialsOutsideAccordion && !isSystemProvider && ( + + )} + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsFlatView/CredentialsFlatView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsFlatView/CredentialsFlatView.tsx new file mode 100644 index 0000000000..4d220a5359 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsFlatView/CredentialsFlatView.tsx @@ -0,0 +1,134 @@ +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip"; +import { + BlockIOCredentialsSubSchema, + CredentialsMetaInput, +} from "@/lib/autogpt-server-api/types"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { CredentialRow } from "../CredentialRow/CredentialRow"; +import { CredentialsSelect } from "../CredentialsSelect/CredentialsSelect"; + +type Credential = { + id: string; + title?: string; + username?: string; + type: string; + provider: string; +}; + +type Props = { + schema: BlockIOCredentialsSubSchema; + provider: string; + displayName: string; + credentials: Credential[]; + selectedCredential?: CredentialsMetaInput; + actionButtonText: string; + isOptional: boolean; + showTitle: boolean; + readOnly: boolean; + variant: "default" | "node"; + onSelectCredential: (credentialId: string) => void; + onClearCredential: () => void; + onAddCredential: () => void; +}; + +export function CredentialsFlatView({ + schema, + provider, + displayName, + credentials, + selectedCredential, + actionButtonText, + isOptional, + showTitle, + readOnly, + variant, + onSelectCredential, + onClearCredential, + onAddCredential, +}: Props) { + const hasCredentials = credentials.length > 0; + + return ( + <> + {showTitle && ( +
+ + + {displayName} credentials + {isOptional && ( + + (optional) + + )} + {!isOptional && !selectedCredential && ( + + + required + + )} + + + {schema.description && ( + + )} +
+ )} + + {hasCredentials ? ( + <> + {(credentials.length > 1 || isOptional) && !readOnly ? ( + + ) : ( +
+ {credentials.map((credential) => ( + onSelectCredential(credential.id)} + readOnly={readOnly} + /> + ))} +
+ )} + {!readOnly && ( + + )} + + ) : ( + !readOnly && ( + + ) + )} + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx index 6e1ec2afb1..18e772dd00 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/components/CredentialsSelect/CredentialsSelect.tsx @@ -1,14 +1,4 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/__legacy__/ui/select"; -import { Text } from "@/components/atoms/Text/Text"; -import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types"; -import { cn } from "@/lib/utils"; -import { useEffect } from "react"; +import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput"; import { getCredentialDisplayName } from "../../helpers"; import { CredentialRow } from "../CredentialRow/CredentialRow"; @@ -42,76 +32,77 @@ export function CredentialsSelect({ allowNone = true, variant = "default", }: Props) { - // Auto-select first credential if none is selected (only if allowNone is false) - useEffect(() => { - if (!allowNone && !selectedCredentials && credentials.length > 0) { - onSelectCredential(credentials[0].id); - } - }, [allowNone, selectedCredentials, credentials, onSelectCredential]); - - const handleValueChange = (value: string) => { + function handleValueChange(e: React.ChangeEvent) { + const value = e.target.value; if (value === "__none__") { onClearCredential?.(); } else { onSelectCredential(value); } - }; + } + + const selectedCredential = selectedCredentials + ? credentials.find((c) => c.id === selectedCredentials.id) + : null; + + const displayCredential = selectedCredential + ? { + id: selectedCredential.id, + title: selectedCredential.title, + username: selectedCredential.username, + type: selectedCredential.type, + provider: selectedCredential.provider, + } + : allowNone + ? { + id: "__none__", + title: "None (skip this credential)", + type: "none", + provider: provider, + } + : { + id: "__placeholder__", + title: "Select credential", + type: "placeholder", + provider: provider, + }; return (
- - {selectedCredentials ? ( - - {}} - onDelete={() => {}} - readOnly={readOnly} - asSelectTrigger={true} - variant={variant} - /> - + {allowNone ? ( + ) : ( - - )} - - - {allowNone && ( - -
- - None (skip this credential) - -
-
+ )} {credentials.map((credential) => ( - -
- - {getCredentialDisplayName(credential, displayName)} - -
-
+ ))} -
- + +
+ {}} + onDelete={() => {}} + readOnly={readOnly} + asSelectTrigger={true} + variant={variant} + /> +
+
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/helpers.ts index 4cca825747..ef965d5382 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/helpers.ts @@ -99,4 +99,30 @@ export function getCredentialDisplayName( } export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; -export const MASKED_KEY_LENGTH = 30; +export const MASKED_KEY_LENGTH = 15; + +export function isSystemCredential(credential: { + title?: string | null; + is_system?: boolean; +}): boolean { + if (credential.is_system === true) return true; + if (!credential.title) return false; + const titleLower = credential.title.toLowerCase(); + return ( + titleLower.includes("system") || + titleLower.startsWith("use credits for") || + titleLower.includes("use credits") + ); +} + +export function filterSystemCredentials< + T extends { title?: string; is_system?: boolean }, +>(credentials: T[]): T[] { + return credentials.filter((cred) => !isSystemCredential(cred)); +} + +export function getSystemCredentials< + T extends { title?: string; is_system?: boolean }, +>(credentials: T[]): T[] { + return credentials.filter((cred) => isSystemCredential(cred)); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts index c780ffeffc..8876ddcba9 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/useCredentialsInput.ts @@ -6,9 +6,11 @@ import { CredentialsMetaInput, } from "@/lib/autogpt-server-api/types"; import { useQueryClient } from "@tanstack/react-query"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { + filterSystemCredentials, getActionButtonText, + getSystemCredentials, OAUTH_TIMEOUT_MS, OAuthPopupResultMessage, } from "./helpers"; @@ -54,6 +56,7 @@ export function useCredentialsInput({ const api = useBackendAPI(); const queryClient = useQueryClient(); const credentials = useCredentials(schema, siblingInputs); + const hasAttemptedAutoSelect = useRef(false); const deleteCredentialsMutation = useDeleteV1DeleteCredentials({ mutation: { @@ -82,38 +85,51 @@ export function useCredentialsInput({ useEffect(() => { if (readOnly) return; if (!credentials || !("savedCredentials" in credentials)) return; + const availableCreds = credentials.savedCredentials; if ( selectedCredential && - !credentials.savedCredentials.some((c) => c.id === selectedCredential.id) + !availableCreds.some((c) => c.id === selectedCredential.id) ) { onSelectCredential(undefined); + // Reset auto-selection flag so it can run again after unsetting invalid credential + hasAttemptedAutoSelect.current = false; } }, [credentials, selectedCredential, onSelectCredential, readOnly]); - // The available credential, if there is only one - const singleCredential = useMemo(() => { - if (!credentials || !("savedCredentials" in credentials)) { - return null; - } - - return credentials.savedCredentials.length === 1 - ? credentials.savedCredentials[0] - : null; - }, [credentials]); - - // Auto-select the one available credential (only if not optional) + // Auto-select the first available credential on initial mount + // Once a user has made a selection, we don't override it useEffect(() => { if (readOnly) return; - if (isOptional) return; // Don't auto-select when credential is optional - if (singleCredential && !selectedCredential) { - onSelectCredential(singleCredential); + if (!credentials || !("savedCredentials" in credentials)) return; + + // If already selected, don't auto-select + if (selectedCredential?.id) return; + + // Only attempt auto-selection once + if (hasAttemptedAutoSelect.current) return; + hasAttemptedAutoSelect.current = true; + + // If optional, don't auto-select (user can choose "None") + if (isOptional) return; + + const savedCreds = credentials.savedCredentials; + + // Auto-select the first credential if any are available + if (savedCreds.length > 0) { + const cred = savedCreds[0]; + onSelectCredential({ + id: cred.id, + type: cred.type, + provider: credentials.provider, + title: (cred as any).title, + }); } }, [ - singleCredential, - selectedCredential, - onSelectCredential, + credentials, + selectedCredential?.id, readOnly, isOptional, + onSelectCredential, ]); if ( @@ -135,8 +151,13 @@ export function useCredentialsInput({ supportsHostScoped, savedCredentials, oAuthCallback, + isSystemProvider, } = credentials; + // Split credentials into user and system + const userCredentials = filterSystemCredentials(savedCredentials); + const systemCredentials = getSystemCredentials(savedCredentials); + async function handleOAuthLogin() { setOAuthError(null); const { login_url, state_token } = await api.oAuthLogin( @@ -291,7 +312,10 @@ export function useCredentialsInput({ supportsOAuth2, supportsUserPassword, supportsHostScoped, - credentialsToShow: savedCredentials, + isSystemProvider, + userCredentials, + systemCredentials, + allCredentials: savedCredentials, selectedCredential, oAuthError, isAPICredentialsModalOpen, @@ -306,7 +330,7 @@ export function useCredentialsInput({ supportsApiKey, supportsUserPassword, supportsHostScoped, - savedCredentials.length > 0, + userCredentials.length > 0, ), setAPICredentialsModalOpen, setUserPasswordCredentialsModalOpen, diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx index e53f31a349..cd0c666be6 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx @@ -12,7 +12,7 @@ import { TooltipTrigger, } from "@/components/atoms/Tooltip/BaseTooltip"; import { Dialog } from "@/components/molecules/Dialog/Dialog"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal"; import { ModalHeader } from "./components/ModalHeader/ModalHeader"; import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection"; @@ -82,6 +82,8 @@ export function RunAgentModal({ }); const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); + const [hasOverflow, setHasOverflow] = useState(false); + const contentRef = useRef(null); const hasAnySetupFields = Object.keys(agentInputFields || {}).length > 0 || @@ -89,6 +91,43 @@ export function RunAgentModal({ const isTriggerRunType = defaultRunType.includes("trigger"); + useEffect(() => { + if (!isOpen) return; + + function checkOverflow() { + if (!contentRef.current) return; + const scrollableParent = contentRef.current + .closest("[data-dialog-content]") + ?.querySelector('[class*="overflow-y-auto"]'); + if (scrollableParent) { + setHasOverflow( + scrollableParent.scrollHeight > scrollableParent.clientHeight, + ); + } + } + + const timeoutId = setTimeout(checkOverflow, 100); + const resizeObserver = new ResizeObserver(checkOverflow); + if (contentRef.current) { + const scrollableParent = contentRef.current + .closest("[data-dialog-content]") + ?.querySelector('[class*="overflow-y-auto"]'); + if (scrollableParent) { + resizeObserver.observe(scrollableParent); + } + } + + return () => { + clearTimeout(timeoutId); + resizeObserver.disconnect(); + }; + }, [ + isOpen, + hasAnySetupFields, + agentInputFields, + agentCredentialsInputFields, + ]); + function handleInputChange(key: string, value: string) { setInputValues((prev) => ({ ...prev, @@ -134,91 +173,97 @@ export function RunAgentModal({ > {triggerSlot} - {/* Header */} - +
+
+ {/* Header */} + - {/* Content */} - {hasAnySetupFields ? ( -
- - - + {/* Content */} + {hasAnySetupFields ? ( +
+ + + +
+ ) : null}
- ) : null} - -
- {isTriggerRunType ? null : !allRequiredInputsAreSet ? ( - - - - - - - - -

- Please set up all required inputs and credentials before - scheduling -

-
-
-
- ) : ( - - )} - +
+ {isTriggerRunType ? null : !allRequiredInputsAreSet ? ( + + + + + + + + +

+ Please set up all required inputs and credentials + before scheduling +

+
+
+
+ ) : ( + + )} + +
+ -
- -
+ +
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/CredentialsGroupedView/CredentialsGroupedView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/CredentialsGroupedView/CredentialsGroupedView.tsx new file mode 100644 index 0000000000..05b2966af7 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/CredentialsGroupedView/CredentialsGroupedView.tsx @@ -0,0 +1,181 @@ +import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/molecules/Accordion/Accordion"; +import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider"; +import { SlidersHorizontal } from "@phosphor-icons/react"; +import { useContext, useEffect, useMemo, useRef } from "react"; +import { useRunAgentModalContext } from "../../context"; +import { + areSystemCredentialProvidersLoading, + CredentialField, + findSavedCredentialByProviderAndType, + hasMissingRequiredSystemCredentials, + splitCredentialFieldsBySystem, +} from "../helpers"; + +type Props = { + credentialFields: CredentialField[]; + requiredCredentials: Set; +}; + +export function CredentialsGroupedView({ + credentialFields, + requiredCredentials, +}: Props) { + const allProviders = useContext(CredentialsProvidersContext); + const { inputCredentials, setInputCredentialsValue, inputValues } = + useRunAgentModalContext(); + + const { userCredentialFields, systemCredentialFields } = useMemo( + () => + splitCredentialFieldsBySystem( + credentialFields, + allProviders, + inputCredentials, + ), + [credentialFields, allProviders, inputCredentials], + ); + + const hasSystemCredentials = systemCredentialFields.length > 0; + const hasUserCredentials = userCredentialFields.length > 0; + const hasAttemptedAutoSelect = useRef(false); + + const isLoadingProviders = useMemo( + () => + areSystemCredentialProvidersLoading(systemCredentialFields, allProviders), + [systemCredentialFields, allProviders], + ); + + const hasMissingSystemCredentials = useMemo(() => { + if (isLoadingProviders) return false; + return hasMissingRequiredSystemCredentials( + systemCredentialFields, + requiredCredentials, + inputCredentials, + allProviders, + ); + }, [ + isLoadingProviders, + systemCredentialFields, + requiredCredentials, + inputCredentials, + allProviders, + ]); + + useEffect(() => { + if (hasAttemptedAutoSelect.current) return; + if (!hasSystemCredentials) return; + if (isLoadingProviders) return; + + for (const [key, schema] of systemCredentialFields) { + const alreadySelected = inputCredentials?.[key]; + const isRequired = requiredCredentials.has(key); + if (alreadySelected || !isRequired) continue; + + const providerNames = schema.credentials_provider || []; + const credentialTypes = schema.credentials_types || []; + const requiredScopes = schema.credentials_scopes; + const savedCredential = findSavedCredentialByProviderAndType( + providerNames, + credentialTypes, + requiredScopes, + allProviders, + ); + + if (savedCredential) { + setInputCredentialsValue(key, { + id: savedCredential.id, + provider: savedCredential.provider, + type: savedCredential.type, + title: (savedCredential as { title?: string }).title, + }); + } + } + + hasAttemptedAutoSelect.current = true; + }, [ + allProviders, + hasSystemCredentials, + systemCredentialFields, + requiredCredentials, + inputCredentials, + setInputCredentialsValue, + isLoadingProviders, + ]); + + return ( +
+ {hasUserCredentials && ( + <> + {userCredentialFields.map( + ([key, inputSubSchema]: CredentialField) => { + const selectedCred = inputCredentials?.[key]; + + return ( + { + setInputCredentialsValue(key, value); + }} + siblingInputs={inputValues} + isOptional={!requiredCredentials.has(key)} + /> + ); + }, + )} + + )} + + {hasSystemCredentials && ( + + + +
+ System credentials + {hasMissingSystemCredentials && ( + (missing) + )} +
+
+ +
+ {systemCredentialFields.map( + ([key, inputSubSchema]: CredentialField) => { + const selectedCred = inputCredentials?.[key]; + + return ( + { + setInputCredentialsValue(key, value); + }} + siblingInputs={inputValues} + isOptional={!requiredCredentials.has(key)} + /> + ); + }, + )} +
+
+
+
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx index aba4caee7a..7660de7c15 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/ModalRunSection/ModalRunSection.tsx @@ -1,8 +1,9 @@ -import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs"; import { Input } from "@/components/atoms/Input/Input"; import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip"; +import { useMemo } from "react"; import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs"; import { useRunAgentModalContext } from "../../context"; +import { CredentialsGroupedView } from "../CredentialsGroupedView/CredentialsGroupedView"; import { ModalSection } from "../ModalSection/ModalSection"; import { WebhookTriggerBanner } from "../WebhookTriggerBanner/WebhookTriggerBanner"; @@ -17,15 +18,16 @@ export function ModalRunSection() { inputValues, setInputValue, agentInputFields, - inputCredentials, - setInputCredentialsValue, agentCredentialsInputFields, } = useRunAgentModalContext(); const inputFields = Object.entries(agentInputFields || {}); - const credentialFields = Object.entries(agentCredentialsInputFields || {}); - // Get the list of required credentials from the schema + const credentialFields = useMemo(() => { + if (!agentCredentialsInputFields) return []; + return Object.entries(agentCredentialsInputFields); + }, [agentCredentialsInputFields]); + const requiredCredentials = new Set( (agent.credentials_input_schema?.required as string[]) || [], ); @@ -97,24 +99,10 @@ export function ModalRunSection() { title="Task Credentials" subtitle="These are the credentials the agent will use to perform this task" > -
- {Object.entries(agentCredentialsInputFields || {}).map( - ([key, inputSubSchema]) => ( - - setInputCredentialsValue(key, value) - } - siblingInputs={inputValues} - isOptional={!requiredCredentials.has(key)} - /> - ), - )} -
+ ) : null}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/helpers.ts new file mode 100644 index 0000000000..61267f733d --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/components/helpers.ts @@ -0,0 +1,210 @@ +import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider"; +import { getSystemCredentials } from "../../CredentialsInputs/helpers"; + +export type CredentialField = [string, any]; + +type SavedCredential = { + id: string; + provider: string; + type: string; + title?: string | null; +}; + +function hasRequiredScopes( + credential: { scopes?: string[]; type: string }, + requiredScopes?: string[], +) { + if (credential.type !== "oauth2") return true; + if (!requiredScopes || requiredScopes.length === 0) return true; + const grantedScopes = new Set(credential.scopes || []); + for (const scope of requiredScopes) { + if (!grantedScopes.has(scope)) return false; + } + return true; +} + +export function splitCredentialFieldsBySystem( + credentialFields: CredentialField[], + allProviders: CredentialsProvidersContextType | null, + inputCredentials?: Record, +) { + if (!allProviders || credentialFields.length === 0) { + return { + userCredentialFields: [] as CredentialField[], + systemCredentialFields: [] as CredentialField[], + }; + } + + const userFields: CredentialField[] = []; + const systemFields: CredentialField[] = []; + + for (const [key, schema] of credentialFields) { + const providerNames = schema.credentials_provider || []; + const isSystemField = providerNames.some((providerName: string) => { + const providerData = allProviders[providerName]; + return providerData?.isSystemProvider === true; + }); + + if (isSystemField) { + systemFields.push([key, schema]); + } else { + userFields.push([key, schema]); + } + } + + const sortByUnsetFirst = (a: CredentialField, b: CredentialField) => { + const aIsSet = Boolean(inputCredentials?.[a[0]]); + const bIsSet = Boolean(inputCredentials?.[b[0]]); + + if (aIsSet === bIsSet) return 0; + return aIsSet ? 1 : -1; + }; + + return { + userCredentialFields: userFields.sort(sortByUnsetFirst), + systemCredentialFields: systemFields.sort(sortByUnsetFirst), + }; +} + +export function areSystemCredentialProvidersLoading( + systemCredentialFields: CredentialField[], + allProviders: CredentialsProvidersContextType | null, +): boolean { + if (!systemCredentialFields.length) return false; + if (allProviders === null) return true; + + for (const [_, schema] of systemCredentialFields) { + const providerNames = schema.credentials_provider || []; + const hasAllProviders = providerNames.every( + (providerName: string) => allProviders?.[providerName] !== undefined, + ); + if (!hasAllProviders) return true; + } + + return false; +} + +export function hasMissingRequiredSystemCredentials( + systemCredentialFields: CredentialField[], + requiredCredentials: Set, + inputCredentials?: Record, + allProviders?: CredentialsProvidersContextType | null, +) { + if (systemCredentialFields.length === 0) return false; + if (allProviders === null) return false; + + return systemCredentialFields.some(([key, schema]) => { + if (!requiredCredentials.has(key)) return false; + if (inputCredentials?.[key]) return false; + + const providerNames = schema.credentials_provider || []; + const credentialTypes = schema.credentials_types || []; + const requiredScopes = schema.credentials_scopes; + + return !hasAvailableSystemCredential( + providerNames, + credentialTypes, + requiredScopes, + allProviders, + ); + }); +} + +function hasAvailableSystemCredential( + providerNames: string[], + credentialTypes: string[], + requiredScopes: string[] | undefined, + allProviders: CredentialsProvidersContextType | null | undefined, +) { + if (!allProviders) return false; + + for (const providerName of providerNames) { + const providerData = allProviders[providerName]; + if (!providerData) continue; + + const systemCredentials = getSystemCredentials( + providerData.savedCredentials ?? [], + ); + + for (const credential of systemCredentials) { + const typeMatches = + credentialTypes.length === 0 || + credentialTypes.includes(credential.type); + const scopesMatch = hasRequiredScopes(credential, requiredScopes); + + if (!typeMatches) continue; + if (!scopesMatch) continue; + + return true; + } + + const allCredentials = providerData.savedCredentials ?? []; + for (const credential of allCredentials) { + const typeMatches = + credentialTypes.length === 0 || + credentialTypes.includes(credential.type); + const scopesMatch = hasRequiredScopes(credential, requiredScopes); + + if (!typeMatches) continue; + if (!scopesMatch) continue; + + return true; + } + } + + return false; +} + +export function findSavedCredentialByProviderAndType( + providerNames: string[], + credentialTypes: string[], + requiredScopes: string[] | undefined, + allProviders: CredentialsProvidersContextType | null, +): SavedCredential | undefined { + for (const providerName of providerNames) { + const providerData = allProviders?.[providerName]; + if (!providerData) continue; + + const systemCredentials = getSystemCredentials( + providerData.savedCredentials ?? [], + ); + + const matchingCredentials: SavedCredential[] = []; + + for (const credential of systemCredentials) { + const typeMatches = + credentialTypes.length === 0 || + credentialTypes.includes(credential.type); + const scopesMatch = hasRequiredScopes(credential, requiredScopes); + + if (!typeMatches) continue; + if (!scopesMatch) continue; + + matchingCredentials.push(credential as SavedCredential); + } + + if (matchingCredentials.length === 0) { + const allCredentials = providerData.savedCredentials ?? []; + for (const credential of allCredentials) { + const typeMatches = + credentialTypes.length === 0 || + credentialTypes.includes(credential.type); + const scopesMatch = hasRequiredScopes(credential, requiredScopes); + + if (!typeMatches) continue; + if (!scopesMatch) continue; + + matchingCredentials.push(credential as SavedCredential); + } + } + + if (matchingCredentials.length === 1) { + return matchingCredentials[0]; + } + if (matchingCredentials.length > 1) { + return undefined; + } + } + + return undefined; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx index eb32083004..3aafd4be50 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/useAgentRunModal.tsx @@ -11,9 +11,18 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset"; import { useToast } from "@/components/molecules/Toast/use-toast"; import { isEmpty } from "@/lib/utils"; +import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider"; import { analytics } from "@/services/analytics"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { getSystemCredentials } from "../CredentialsInputs/helpers"; import { showExecutionErrorToast } from "./errorHelpers"; export type RunVariant = @@ -42,8 +51,10 @@ export function useAgentRunModal( const [inputCredentials, setInputCredentials] = useState>( callbacks?.initialInputCredentials || {}, ); + const [presetName, setPresetName] = useState(""); const [presetDescription, setPresetDescription] = useState(""); + const hasInitializedSystemCreds = useRef(false); // Determine the default run type based on agent capabilities const defaultRunType: RunVariant = agent.trigger_setup_info @@ -58,6 +69,91 @@ export function useAgentRunModal( setInputCredentials(callbacks?.initialInputCredentials || {}); }, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]); + const allProviders = useContext(CredentialsProvidersContext); + + // Initialize credentials with default system credentials + useEffect(() => { + if (!allProviders || !agent.credentials_input_schema?.properties) return; + if (callbacks?.initialInputCredentials) { + hasInitializedSystemCreds.current = true; + return; + } + if (hasInitializedSystemCreds.current) return; + + const properties = agent.credentials_input_schema.properties as Record< + string, + any + >; + + setInputCredentials((currentCreds) => { + const credsToAdd: Record = {}; + + for (const [key, schema] of Object.entries(properties)) { + if (currentCreds[key]) continue; + + const providerNames = schema.credentials_provider || []; + const supportedTypes = schema.credentials_types || []; + const requiredScopes = schema.credentials_scopes; + + for (const providerName of providerNames) { + const providerData = allProviders[providerName]; + if (!providerData) continue; + + const systemCreds = getSystemCredentials( + providerData.savedCredentials ?? [], + ); + const matchingSystemCreds = systemCreds.filter((cred) => { + if (!supportedTypes.includes(cred.type)) return false; + + if ( + cred.type === "oauth2" && + requiredScopes && + requiredScopes.length > 0 + ) { + const grantedScopes = new Set(cred.scopes || []); + const hasAllRequiredScopes = requiredScopes.every( + (scope: string) => grantedScopes.has(scope), + ); + if (!hasAllRequiredScopes) return false; + } + + return true; + }); + + if (matchingSystemCreds.length === 1) { + const systemCred = matchingSystemCreds[0]; + credsToAdd[key] = { + id: systemCred.id, + type: systemCred.type, + provider: providerName, + title: systemCred.title, + }; + break; + } + } + } + + if (Object.keys(credsToAdd).length > 0) { + hasInitializedSystemCreds.current = true; + return { + ...currentCreds, + ...credsToAdd, + }; + } + + return currentCreds; + }); + }, [ + allProviders, + agent.credentials_input_schema, + callbacks?.initialInputCredentials, + ]); + + // Reset initialization flag when modal closes/opens or agent changes + useEffect(() => { + hasInitializedSystemCreds.current = false; + }, [isOpen, agent.graph_id]); + // API mutations const executeGraphMutation = usePostV1ExecuteGraphAgent({ mutation: { @@ -66,7 +162,6 @@ export function useAgentRunModal( toast({ title: "Agent execution started", }); - // Invalidate runs list for this graph queryClient.invalidateQueries({ queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id), }); @@ -163,14 +258,10 @@ export function useAgentRunModal( }, [agentInputSchema.required, inputValues]); const [allCredentialsAreSet, missingCredentials] = useMemo(() => { - // Only check required credentials from schema, not all properties - // Credentials marked as optional in node metadata won't be in the required array const requiredCredentials = new Set( (agent.credentials_input_schema?.required as string[]) || [], ); - // Check if required credentials have valid id (not just key existence) - // A credential is valid only if it has an id field set const missing = [...requiredCredentials].filter((key) => { const cred = inputCredentials[key]; return !cred || !cred.id; @@ -184,7 +275,6 @@ export function useAgentRunModal( [agentCredentialsInputFields], ); - // Final readiness flag combining inputs + credentials when credentials are shown const allRequiredInputsAreSet = useMemo( () => allRequiredInputsAreSetRaw && @@ -223,7 +313,6 @@ export function useAgentRunModal( defaultRunType === "automatic-trigger" || defaultRunType === "manual-trigger" ) { - // Setup trigger if (!presetName.trim()) { toast({ title: "⚠️ Trigger name required", @@ -244,9 +333,6 @@ export function useAgentRunModal( }, }); } else { - // Manual execution - // Filter out incomplete credentials (optional ones not selected) - // Only send credentials that have a valid id field const validCredentials = Object.fromEntries( Object.entries(inputCredentials).filter(([_, cred]) => cred && cred.id), ); @@ -280,41 +366,24 @@ export function useAgentRunModal( }, [agentInputFields]); return { - // UI state isOpen, setIsOpen, - - // Run mode defaultRunType: defaultRunType as RunVariant, - - // Form: regular inputs inputValues, setInputValues, - - // Form: credentials inputCredentials, setInputCredentials, - - // Preset/trigger labels presetName, presetDescription, setPresetName, setPresetDescription, - - // Validation/readiness allRequiredInputsAreSet, missingInputs, - - // Schemas for rendering agentInputFields, agentCredentialsInputFields, hasInputFields, - - // Async states isExecuting: executeGraphMutation.isPending, isSettingUpTrigger: setupTriggerMutation.isPending, - - // Actions handleRun, }; } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton.tsx index 11dcbd943f..95fdf826a2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton.tsx @@ -1,37 +1,17 @@ import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; import { GearIcon } from "@phosphor-icons/react"; -import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; -import { useAgentSafeMode } from "@/hooks/useAgentSafeMode"; - -interface Props { - agent: LibraryAgent; - onSelectSettings: () => void; - selected?: boolean; -} - -export function AgentSettingsButton({ - agent, - onSelectSettings, - selected, -}: Props) { - const { hasHITLBlocks } = useAgentSafeMode(agent); - - if (!hasHITLBlocks) { - return null; - } +export function AgentSettingsButton() { return ( ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptySchedules.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptySchedules.tsx index 97492d8a59..4c781b2896 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptySchedules.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptySchedules.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Text } from "@/components/atoms/Text/Text"; export function EmptySchedules() { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTemplates.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTemplates.tsx index c33abe69ad..364b762167 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTemplates.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTemplates.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Text } from "@/components/atoms/Text/Text"; export function EmptyTemplates() { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTriggers.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTriggers.tsx index 0d9dc47fff..06d09ff9a0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTriggers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/EmptyTriggers.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Text } from "@/components/atoms/Text/Text"; export function EmptyTriggers() { diff --git a/autogpt_platform/frontend/src/components/contextual/MarketplaceBanners/MarketplaceBanners.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/MarketplaceBanners.tsx similarity index 97% rename from autogpt_platform/frontend/src/components/contextual/MarketplaceBanners/MarketplaceBanners.tsx rename to autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/MarketplaceBanners.tsx index 4f826f6e85..00edcc721f 100644 --- a/autogpt_platform/frontend/src/components/contextual/MarketplaceBanners/MarketplaceBanners.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/MarketplaceBanners.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; -interface MarketplaceBannersProps { +interface Props { hasUpdate?: boolean; latestVersion?: number; hasUnpublishedChanges?: boolean; @@ -21,7 +21,7 @@ export function MarketplaceBanners({ isUpdating, onUpdate, onPublish, -}: MarketplaceBannersProps) { +}: Props) { const renderUpdateBanner = () => { if (hasUpdate && latestVersion) { return ( diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/SectionWrap.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/SectionWrap.tsx index 75571dd856..f88d91bb0d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/SectionWrap.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/SectionWrap.tsx @@ -1,3 +1,5 @@ +"use client"; + import { cn } from "@/lib/utils"; type Props = { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/LoadingSelectedContent.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/LoadingSelectedContent.tsx index dc2bb7cac2..bc5548afd0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/LoadingSelectedContent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/LoadingSelectedContent.tsx @@ -1,22 +1,16 @@ +import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { Skeleton } from "@/components/__legacy__/ui/skeleton"; import { cn } from "@/lib/utils"; -import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers"; import { SelectedViewLayout } from "./SelectedViewLayout"; interface Props { agent: LibraryAgent; - onSelectSettings?: () => void; - selectedSettings?: boolean; } export function LoadingSelectedContent(props: Props) { return ( - +
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx index c66f0e9245..05da986583 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx @@ -33,8 +33,6 @@ interface Props { onSelectRun?: (id: string) => void; onClearSelectedRun?: () => void; banner?: React.ReactNode; - onSelectSettings?: () => void; - selectedSettings?: boolean; } export function SelectedRunView({ @@ -43,8 +41,6 @@ export function SelectedRunView({ onSelectRun, onClearSelectedRun, banner, - onSelectSettings, - selectedSettings, }: Props) { const { run, preset, isLoading, responseError, httpError } = useSelectedRunView(agent.graph_id, runId); @@ -84,12 +80,7 @@ export function SelectedRunView({ return (
- +
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx index 445394c44a..e0a81dba5f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx @@ -21,8 +21,6 @@ interface Props { scheduleId: string; onClearSelectedRun?: () => void; banner?: React.ReactNode; - onSelectSettings?: () => void; - selectedSettings?: boolean; } export function SelectedScheduleView({ @@ -30,8 +28,6 @@ export function SelectedScheduleView({ scheduleId, onClearSelectedRun, banner, - onSelectSettings, - selectedSettings, }: Props) { const { schedule, isLoading, error } = useSelectedScheduleView( agent.graph_id, @@ -76,12 +72,7 @@ export function SelectedScheduleView({ return (
- +
{}}> +
Agent Settings
-
- {!hasHITLBlocks ? ( -
- - This agent doesn't have any human-in-the-loop blocks, so - there are no settings to configure. - -
- ) : ( +
+ {hasHITLBlocks ? (
@@ -59,6 +52,12 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) { />
+ ) : ( +
+ + This agent doesn't have any configurable settings. + +
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTemplateView/SelectedTemplateView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTemplateView/SelectedTemplateView.tsx index b5ecb7ae5c..d0c49c2a93 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTemplateView/SelectedTemplateView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTemplateView/SelectedTemplateView.tsx @@ -8,7 +8,7 @@ import { getAgentCredentialsFields, getAgentInputFields, } from "../../modals/AgentInputsReadOnly/helpers"; -import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInputs"; +import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInput"; import { RunAgentInputs } from "../../modals/RunAgentInputs/RunAgentInputs"; import { LoadingSelectedContent } from "../LoadingSelectedContent"; import { RunDetailCard } from "../RunDetailCard/RunDetailCard"; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTriggerView/SelectedTriggerView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTriggerView/SelectedTriggerView.tsx index f92c91112e..0d0cdc95cc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTriggerView/SelectedTriggerView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedTriggerView/SelectedTriggerView.tsx @@ -7,7 +7,7 @@ import { getAgentCredentialsFields, getAgentInputFields, } from "../../modals/AgentInputsReadOnly/helpers"; -import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInputs"; +import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInput"; import { RunAgentInputs } from "../../modals/RunAgentInputs/RunAgentInputs"; import { LoadingSelectedContent } from "../LoadingSelectedContent"; import { RunDetailCard } from "../RunDetailCard/RunDetailCard"; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedViewLayout.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedViewLayout.tsx index 8a4e46a606..fe824604df 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedViewLayout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedViewLayout.tsx @@ -1,7 +1,7 @@ -import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs"; -import { AgentSettingsButton } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs"; import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers"; +import { AgentSettingsModal } from "../modals/AgentSettingsModal/AgentSettingsModal"; import { SectionWrap } from "../other/SectionWrap"; interface Props { @@ -9,8 +9,6 @@ interface Props { children: React.ReactNode; banner?: React.ReactNode; additionalBreadcrumb?: { name: string; link?: string }; - onSelectSettings?: () => void; - selectedSettings?: boolean; } export function SelectedViewLayout(props: Props) { @@ -19,8 +17,8 @@ export function SelectedViewLayout(props: Props) {
- {props.banner &&
{props.banner}
} -
+ {props.banner} +
- {props.agent && props.onSelectSettings && ( -
- -
- )} +
+ +
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx index 5f57032618..1b155543f1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx @@ -12,7 +12,7 @@ import { } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs"; +import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput"; import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentInputs/RunAgentInputs"; import { ScheduleTaskDialog } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/cron-scheduler-dialog"; import ActionButtonGroup from "@/components/__legacy__/action-button-group"; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx index 9ada590dd8..147c0aef45 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx @@ -1,14 +1,7 @@ "use client"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { NewAgentLibraryView } from "./components/NewAgentLibraryView/NewAgentLibraryView"; -import { OldAgentLibraryView } from "./components/OldAgentLibraryView/OldAgentLibraryView"; export default function AgentLibraryPage() { - const isNewLibraryPageEnabled = useGetFlag(Flag.NEW_AGENT_RUNS); - return isNewLibraryPageEnabled ? ( - - ) : ( - - ); + return ; } diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index e601be6626..6f9a87216b 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -2870,6 +2870,28 @@ } } }, + "/api/integrations/providers/system": { + "get": { + "tags": ["v1", "integrations"], + "summary": "List System Providers", + "description": "Get a list of providers that have platform credits (system credentials) available.\n\nThese providers can be used without the user providing their own API keys.", + "operationId": "getV1ListSystemProviders", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { "type": "string" }, + "type": "array", + "title": "Response Getv1Listsystemproviders" + } + } + } + } + } + } + }, "/api/integrations/webhooks/{webhook_id}/ping": { "post": { "tags": ["v1", "integrations"], diff --git a/autogpt_platform/frontend/src/components/atoms/Button/Button.tsx b/autogpt_platform/frontend/src/components/atoms/Button/Button.tsx index de1dec2d25..ab7b90e098 100644 --- a/autogpt_platform/frontend/src/components/atoms/Button/Button.tsx +++ b/autogpt_platform/frontend/src/components/atoms/Button/Button.tsx @@ -20,6 +20,7 @@ export function Button(props: ButtonProps) { rightIcon, children, as = "button", + asChild: _asChild, // Destructure to prevent passing to DOM ...restProps } = props; diff --git a/autogpt_platform/frontend/src/components/contextual/GoogleDrivePicker/GoogleDrivePicker.tsx b/autogpt_platform/frontend/src/components/contextual/GoogleDrivePicker/GoogleDrivePicker.tsx index e0a43b8c77..4decd2dbdb 100644 --- a/autogpt_platform/frontend/src/components/contextual/GoogleDrivePicker/GoogleDrivePicker.tsx +++ b/autogpt_platform/frontend/src/components/contextual/GoogleDrivePicker/GoogleDrivePicker.tsx @@ -1,6 +1,6 @@ "use client"; -import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs"; +import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput"; import { Button } from "@/components/atoms/Button/Button"; import { CircleNotchIcon, FolderOpenIcon } from "@phosphor-icons/react"; import { diff --git a/autogpt_platform/frontend/src/components/molecules/Accordion/Accordion.stories.tsx b/autogpt_platform/frontend/src/components/molecules/Accordion/Accordion.stories.tsx new file mode 100644 index 0000000000..d0fce53e0e --- /dev/null +++ b/autogpt_platform/frontend/src/components/molecules/Accordion/Accordion.stories.tsx @@ -0,0 +1,203 @@ +import type { Meta } from "@storybook/nextjs"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "./Accordion"; + +const meta: Meta = { + title: "Molecules/Accordion", + component: Accordion, + parameters: { + layout: "centered", + docs: { + description: { + component: ` +## Accordion Component + +A vertically stacked set of interactive headings that each reveal an associated section of content. + +### ✨ Features + +- **Built on Radix UI** - Uses @radix-ui/react-accordion for accessibility and functionality +- **Single or multiple** - Supports single or multiple items open at once +- **Smooth animations** - Built-in expand/collapse animations +- **Accessible** - Full keyboard navigation and screen reader support +- **Customizable** - Style with Tailwind CSS classes + +### 🎯 Usage + +\`\`\`tsx + + + Is it accessible? + + Yes. It adheres to the WAI-ARIA design pattern. + + + +\`\`\` + +### Props + +**Accordion**: +- **type**: "single" | "multiple" - Whether one or multiple items can be open +- **collapsible**: boolean - When type is "single", allows closing all items +- **defaultValue**: string | string[] - Default open item(s) +- **value**: string | string[] - Controlled open item(s) +- **onValueChange**: (value) => void - Callback when value changes + +**AccordionItem**: +- **value**: string - Unique identifier for the item +- **disabled**: boolean - Whether the item is disabled + +**AccordionTrigger**: +- Standard button props + +**AccordionContent**: +- Standard div props + `, + }, + }, + }, + tags: ["autodocs"], + argTypes: { + type: { + control: "radio", + options: ["single", "multiple"], + description: "Whether one or multiple items can be open at the same time", + table: { + defaultValue: { summary: "single" }, + }, + }, + collapsible: { + control: "boolean", + description: + 'When type is "single", allows closing content when clicking on open trigger', + table: { + defaultValue: { summary: "false" }, + }, + }, + }, +}; + +export default meta; + +export function Default() { + return ( + + + Is it accessible? + + Yes. It adheres to the WAI-ARIA design pattern. + + + + Is it styled? + + Yes. It comes with default styles that match your design system. + + + + Is it animated? + + Yes. It's animated by default with smooth expand/collapse + transitions. + + + + ); +} + +export function Multiple() { + return ( + + + First section + + Multiple items can be open at the same time when type is set to + "multiple". + + + + Second section + + Try opening this one while the first is still open. + + + + Third section + + All three can be open simultaneously. + + + + ); +} + +export function DefaultOpen() { + return ( + + + Closed by default + This item starts closed. + + + Open by default + + This item starts open because defaultValue is set to + "item-2". + + + + Also closed + This item also starts closed. + + + ); +} + +export function WithDisabledItem() { + return ( + + + Available item + This item can be toggled. + + + Disabled item + + This content cannot be accessed because the item is disabled. + + + + Another available item + This item can also be toggled. + + + ); +} + +export function CustomStyled() { + return ( + + + + Custom styled trigger + + + You can customize the styling using className props. + + + + + Blue themed + + + Each item can have different styles. + + + + ); +} diff --git a/autogpt_platform/frontend/src/components/molecules/Accordion/Accordion.tsx b/autogpt_platform/frontend/src/components/molecules/Accordion/Accordion.tsx new file mode 100644 index 0000000000..b071fc1d37 --- /dev/null +++ b/autogpt_platform/frontend/src/components/molecules/Accordion/Accordion.tsx @@ -0,0 +1,8 @@ +"use client"; + +export { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; diff --git a/autogpt_platform/frontend/src/components/molecules/Dialog/components/DrawerWrap.tsx b/autogpt_platform/frontend/src/components/molecules/Dialog/components/DrawerWrap.tsx index d00817bf59..3bfa321538 100644 --- a/autogpt_platform/frontend/src/components/molecules/Dialog/components/DrawerWrap.tsx +++ b/autogpt_platform/frontend/src/components/molecules/Dialog/components/DrawerWrap.tsx @@ -22,6 +22,9 @@ export function DrawerWrap({ handleClose, isForceOpen, }: Props) { + const accessibleTitle = title ?? "Dialog"; + const hasVisibleTitle = Boolean(title); + const closeBtn = (