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 53a7e8b860..0147c19a5c 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 @@ -1,8 +1,15 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; import { CredentialsMetaInput, + CredentialsType, GraphExecutionID, GraphMeta, LibraryAgentPreset, @@ -29,7 +36,11 @@ import { } from "@/components/__legacy__/ui/icons"; import { Input } from "@/components/__legacy__/ui/input"; import { Button } from "@/components/atoms/Button/Button"; -import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput"; +import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView"; +import { + findSavedCredentialByProviderAndType, + findSavedUserCredentialByProviderAndType, +} from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers"; import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip"; import { useToast, @@ -37,6 +48,7 @@ import { } from "@/components/molecules/Toast/use-toast"; import { humanizeCronExpression } from "@/lib/cron-expression-utils"; import { cn, isEmpty } from "@/lib/utils"; +import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider"; import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react"; import { CalendarClockIcon, Trash2Icon } from "lucide-react"; @@ -90,6 +102,7 @@ export function AgentRunDraftView({ const api = useBackendAPI(); const { toast } = useToast(); const toastOnFail = useToastOnFail(); + const allProviders = useContext(CredentialsProvidersContext); const [inputValues, setInputValues] = useState>({}); const [inputCredentials, setInputCredentials] = useState< @@ -128,6 +141,77 @@ export function AgentRunDraftView({ () => graph.credentials_input_schema.properties, [graph], ); + const credentialFields = useMemo( + function getCredentialFields() { + return Object.entries(agentCredentialsInputFields); + }, + [agentCredentialsInputFields], + ); + const requiredCredentials = useMemo( + function getRequiredCredentials() { + return new Set( + (graph.credentials_input_schema?.required as string[]) || [], + ); + }, + [graph.credentials_input_schema?.required], + ); + + useEffect( + function initializeDefaultCredentials() { + if (!allProviders) return; + if (!graph.credentials_input_schema?.properties) return; + if (requiredCredentials.size === 0) return; + + setInputCredentials(function updateCredentials(currentCreds) { + const next = { ...currentCreds }; + let didAdd = false; + + for (const key of requiredCredentials) { + if (next[key]) continue; + const schema = graph.credentials_input_schema.properties[key]; + if (!schema) continue; + + const providerNames = schema.credentials_provider || []; + const credentialTypes = schema.credentials_types || []; + const requiredScopes = schema.credentials_scopes; + + const userCredential = findSavedUserCredentialByProviderAndType( + providerNames, + credentialTypes, + requiredScopes, + allProviders, + ); + + const savedCredential = + userCredential || + findSavedCredentialByProviderAndType( + providerNames, + credentialTypes, + requiredScopes, + allProviders, + ); + + if (!savedCredential) continue; + + next[key] = { + id: savedCredential.id, + provider: savedCredential.provider, + type: savedCredential.type as CredentialsType, + title: savedCredential.title, + }; + didAdd = true; + } + + if (!didAdd) return currentCreds; + return next; + }); + }, + [ + allProviders, + graph.credentials_input_schema?.properties, + requiredCredentials, + ], + ); const [allRequiredInputsAreSet, missingInputs] = useMemo(() => { const nonEmptyInputs = new Set( @@ -145,18 +229,35 @@ export function AgentRunDraftView({ ); return [isSuperset, difference]; }, [agentInputSchema.required, inputValues]); - const [allCredentialsAreSet, missingCredentials] = useMemo(() => { - const availableCredentials = new Set(Object.keys(inputCredentials)); - const allCredentials = new Set(Object.keys(agentCredentialsInputFields)); - // Backwards-compatible implementation of isSupersetOf and difference - const isSuperset = Array.from(allCredentials).every((item) => - availableCredentials.has(item), - ); - const difference = Array.from(allCredentials).filter( - (item) => !availableCredentials.has(item), - ); - return [isSuperset, difference]; - }, [agentCredentialsInputFields, inputCredentials]); + const [allCredentialsAreSet, missingCredentials] = useMemo( + function getCredentialStatus() { + const missing = Array.from(requiredCredentials).filter((key) => { + const cred = inputCredentials[key]; + return !cred || !cred.id; + }); + return [missing.length === 0, missing]; + }, + [requiredCredentials, inputCredentials], + ); + function addChangedCredentials(prev: Set) { + const next = new Set(prev); + next.add("credentials"); + return next; + } + + function handleCredentialChange(key: string, value?: CredentialsMetaInput) { + setInputCredentials(function updateInputCredentials(currentCreds) { + const next = { ...currentCreds }; + if (value === undefined) { + delete next[key]; + return next; + } + next[key] = value; + return next; + }); + setChangedPresetAttributes(addChangedCredentials); + } + const notifyMissingInputs = useCallback( (needPresetName: boolean = true) => { const allMissingFields = ( @@ -649,35 +750,6 @@ export function AgentRunDraftView({ )} - {/* Credentials inputs */} - {Object.entries(agentCredentialsInputFields).map( - ([key, inputSubSchema]) => ( - { - setInputCredentials((obj) => { - const newObj = { ...obj }; - if (value === undefined) { - delete newObj[key]; - return newObj; - } - return { - ...obj, - [key]: value, - }; - }); - setChangedPresetAttributes((prev) => - prev.add("credentials"), - ); - }} - /> - ), - )} - {/* Regular inputs */} {Object.entries(agentInputFields).map(([key, inputSubSchema]) => ( ))} + + {/* Credentials inputs */} + {credentialFields.length > 0 && ( + + )} diff --git a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers.ts b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers.ts index 519ef302c1..5f439d3a32 100644 --- a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers.ts +++ b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers.ts @@ -1,5 +1,5 @@ import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider"; -import { getSystemCredentials } from "../../helpers"; +import { filterSystemCredentials, getSystemCredentials } from "../../helpers"; export type CredentialField = [string, any]; @@ -208,3 +208,42 @@ export function findSavedCredentialByProviderAndType( return undefined; } + +export function findSavedUserCredentialByProviderAndType( + 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 userCredentials = filterSystemCredentials( + providerData.savedCredentials ?? [], + ); + + const matchingCredentials: SavedCredential[] = []; + + for (const credential of userCredentials) { + 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/components/contextual/CredentialsInput/useCredentialsInput.ts b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/useCredentialsInput.ts index 8876ddcba9..509713ff1e 100644 --- a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/useCredentialsInput.ts +++ b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/useCredentialsInput.ts @@ -98,24 +98,20 @@ export function useCredentialsInput({ // 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 (!credentials || !("savedCredentials" in credentials)) return; + useEffect( + function autoSelectCredential() { + if (readOnly) return; + if (!credentials || !("savedCredentials" in credentials)) return; + if (selectedCredential?.id) return; - // If already selected, don't auto-select - if (selectedCredential?.id) return; + const savedCreds = credentials.savedCredentials; + if (savedCreds.length === 0) return; - // Only attempt auto-selection once - if (hasAttemptedAutoSelect.current) return; - hasAttemptedAutoSelect.current = true; + if (hasAttemptedAutoSelect.current) return; + hasAttemptedAutoSelect.current = true; - // If optional, don't auto-select (user can choose "None") - if (isOptional) return; + 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, @@ -123,14 +119,15 @@ export function useCredentialsInput({ provider: credentials.provider, title: (cred as any).title, }); - } - }, [ - credentials, - selectedCredential?.id, - readOnly, - isOptional, - onSelectCredential, - ]); + }, + [ + credentials, + selectedCredential?.id, + readOnly, + isOptional, + onSelectCredential, + ], + ); if ( !credentials || diff --git a/autogpt_platform/frontend/src/lib/timezone-utils.ts b/autogpt_platform/frontend/src/lib/timezone-utils.ts index d2b0c55102..b6cb6b2a10 100644 --- a/autogpt_platform/frontend/src/lib/timezone-utils.ts +++ b/autogpt_platform/frontend/src/lib/timezone-utils.ts @@ -106,9 +106,14 @@ export function getTimezoneDisplayName(timezone: string): string { const parts = timezone.split("/"); const city = parts[parts.length - 1].replace(/_/g, " "); const abbr = getTimezoneAbbreviation(timezone); - return abbr ? `${city} (${abbr})` : city; + if (abbr && abbr !== timezone) { + return `${city} (${abbr})`; + } + // If abbreviation is same as timezone or not found, show timezone with underscores replaced + const timezoneDisplay = timezone.replace(/_/g, " "); + return `${city} (${timezoneDisplay})`; } catch { - return timezone; + return timezone.replace(/_/g, " "); } }