mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-13 17:18:08 -05:00
Compare commits
1 Commits
fix/creden
...
fix/run-mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
383e22da19 |
@@ -198,24 +198,9 @@ async def callback(
|
||||
async def list_credentials(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> list[CredentialsMetaResponse]:
|
||||
from backend.integrations.credentials_store import (
|
||||
DEFAULT_CREDENTIALS,
|
||||
is_system_credential,
|
||||
)
|
||||
from backend.integrations.credentials_store import is_system_credential
|
||||
|
||||
# Get user credentials and configured system credentials
|
||||
credentials = await creds_manager.store.get_all_creds(user_id)
|
||||
|
||||
# Create a set of credential IDs we've already included
|
||||
included_ids = {cred.id for cred in credentials}
|
||||
|
||||
# Always include all system credentials, even if not configured
|
||||
# This ensures the frontend can identify system credentials
|
||||
for system_cred in DEFAULT_CREDENTIALS:
|
||||
if system_cred.id not in included_ids:
|
||||
credentials.append(system_cred)
|
||||
included_ids.add(system_cred.id)
|
||||
|
||||
return [
|
||||
CredentialsMetaResponse(
|
||||
id=cred.id,
|
||||
@@ -238,23 +223,9 @@ async def list_credentials_by_provider(
|
||||
],
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> list[CredentialsMetaResponse]:
|
||||
from backend.integrations.credentials_store import (
|
||||
DEFAULT_CREDENTIALS,
|
||||
is_system_credential,
|
||||
)
|
||||
from backend.integrations.credentials_store import is_system_credential
|
||||
|
||||
# Get user credentials and configured system credentials for this provider
|
||||
credentials = await creds_manager.store.get_creds_by_provider(user_id, provider)
|
||||
|
||||
# Create a set of credential IDs we've already included
|
||||
included_ids = {cred.id for cred in credentials}
|
||||
|
||||
# Always include system credentials for this provider, even if not configured
|
||||
for system_cred in DEFAULT_CREDENTIALS:
|
||||
if system_cred.provider == provider and system_cred.id not in included_ids:
|
||||
credentials.append(system_cred)
|
||||
included_ids.add(system_cred.id)
|
||||
|
||||
return [
|
||||
CredentialsMetaResponse(
|
||||
id=cred.id,
|
||||
@@ -872,18 +843,6 @@ async def list_providers() -> List[str]:
|
||||
return all_providers
|
||||
|
||||
|
||||
@router.get("/providers/system", response_model=List[str])
|
||||
async def list_system_providers() -> List[str]:
|
||||
"""
|
||||
Get a list of providers that have platform credits (system credentials) available.
|
||||
|
||||
These providers can be used without the user providing their own API keys.
|
||||
"""
|
||||
from backend.integrations.credentials_store import SYSTEM_PROVIDERS
|
||||
|
||||
return list(SYSTEM_PROVIDERS)
|
||||
|
||||
|
||||
@router.get("/providers/names", response_model=ProviderNamesResponse)
|
||||
async def get_provider_names() -> ProviderNamesResponse:
|
||||
"""
|
||||
|
||||
@@ -247,20 +247,12 @@ DEFAULT_CREDENTIALS = [
|
||||
|
||||
SYSTEM_CREDENTIAL_IDS = {cred.id for cred in DEFAULT_CREDENTIALS}
|
||||
|
||||
# Set of providers that have system credentials available
|
||||
SYSTEM_PROVIDERS = {cred.provider for cred in DEFAULT_CREDENTIALS}
|
||||
|
||||
|
||||
def is_system_credential(credential_id: str) -> bool:
|
||||
"""Check if a credential ID belongs to a system-managed credential."""
|
||||
return credential_id in SYSTEM_CREDENTIAL_IDS
|
||||
|
||||
|
||||
def is_system_provider(provider: str) -> bool:
|
||||
"""Check if a provider has system-managed credentials available."""
|
||||
return provider in SYSTEM_PROVIDERS
|
||||
|
||||
|
||||
class IntegrationCredentialsStore:
|
||||
def __init__(self):
|
||||
self._locks = None
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@next/third-parties": "15.4.6",
|
||||
"@phosphor-icons/react": "2.1.10",
|
||||
"@radix-ui/react-accordion": "1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||
"@radix-ui/react-avatar": "1.1.10",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
|
||||
49
autogpt_platform/frontend/pnpm-lock.yaml
generated
49
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -20,9 +20,6 @@ importers:
|
||||
'@phosphor-icons/react':
|
||||
specifier: 2.1.10
|
||||
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-accordion':
|
||||
specifier: 1.2.12
|
||||
version: 1.2.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: 1.1.15
|
||||
version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -1829,19 +1826,6 @@ packages:
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
'@radix-ui/react-accordion@1.2.12':
|
||||
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.15':
|
||||
resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
|
||||
peerDependencies:
|
||||
@@ -9149,23 +9133,6 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.17
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.17)
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -12128,8 +12095,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@@ -12148,7 +12115,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -12159,22 +12126,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -12185,7 +12152,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/molecules/Alert/Alert";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -25,6 +31,7 @@ import { SelectedTriggerView } from "./components/selected-views/SelectedTrigger
|
||||
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
||||
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
||||
import { useAgentMissingCredentials } from "./hooks/useAgentMissingCredentials";
|
||||
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
|
||||
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
||||
|
||||
@@ -61,6 +68,10 @@ export function NewAgentLibraryView() {
|
||||
} = useMarketplaceUpdate({ agent });
|
||||
|
||||
const [changelogOpen, setChangelogOpen] = useState(false);
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
const { hasMissingCredentials, isLoading: isLoadingCredentials } =
|
||||
useAgentMissingCredentials(agent);
|
||||
|
||||
useEffect(() => {
|
||||
if (agent) {
|
||||
@@ -145,6 +156,23 @@ export function NewAgentLibraryView() {
|
||||
/>
|
||||
<AgentSettingsModal agent={agent} />
|
||||
</div>
|
||||
{hasMissingCredentials && !isLoadingCredentials && (
|
||||
<Alert variant="warning">
|
||||
<AlertTitle>Missing credentials</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Text variant="small" className="text-zinc-800">
|
||||
This agent requires credentials that are not configured.{" "}
|
||||
<button
|
||||
onClick={() => setSettingsModalOpen(true)}
|
||||
className="font-medium underline hover:no-underline"
|
||||
>
|
||||
Configure credentials
|
||||
</button>{" "}
|
||||
to run tasks.
|
||||
</Text>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<EmptyTasks
|
||||
@@ -155,6 +183,13 @@ export function NewAgentLibraryView() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{agent && (
|
||||
<AgentSettingsModal
|
||||
agent={agent}
|
||||
controlledOpen={settingsModalOpen}
|
||||
onOpenChange={setSettingsModalOpen}
|
||||
/>
|
||||
)}
|
||||
{renderPublishAgentModal()}
|
||||
{renderVersionChangelog()}
|
||||
</>
|
||||
@@ -165,6 +200,25 @@ export function NewAgentLibraryView() {
|
||||
<>
|
||||
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
|
||||
<SectionWrap className="mb-3 block">
|
||||
{hasMissingCredentials && !isLoadingCredentials && (
|
||||
<div className={cn("mb-4", AGENT_LIBRARY_SECTION_PADDING_X)}>
|
||||
<Alert variant="warning">
|
||||
<AlertTitle>Missing credentials</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Text variant="small" className="text-zinc-800">
|
||||
This agent requires credentials that are not configured.{" "}
|
||||
<button
|
||||
onClick={() => setSettingsModalOpen(true)}
|
||||
className="font-medium underline hover:no-underline"
|
||||
>
|
||||
Configure credentials
|
||||
</button>{" "}
|
||||
to run tasks.
|
||||
</Text>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"border-b border-zinc-100 pb-5",
|
||||
@@ -274,6 +328,13 @@ export function NewAgentLibraryView() {
|
||||
</SelectedViewLayout>
|
||||
)}
|
||||
</div>
|
||||
{agent && (
|
||||
<AgentSettingsModal
|
||||
agent={agent}
|
||||
controlledOpen={settingsModalOpen}
|
||||
onOpenChange={setSettingsModalOpen}
|
||||
/>
|
||||
)}
|
||||
{renderPublishAgentModal()}
|
||||
{renderVersionChangelog()}
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import { GearIcon } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useAgentSystemCredentials } from "../../../hooks/useAgentSystemCredentials";
|
||||
import { SystemCredentialRow } from "../../selected-views/SelectedSettingsView/components/SystemCredentialRow";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
@@ -34,7 +36,19 @@ export function AgentSettingsModal({
|
||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||
useAgentSafeMode(agent);
|
||||
|
||||
if (!hasHITLBlocks) return null;
|
||||
const { hasSystemCredentials, systemCredentials } =
|
||||
useAgentSystemCredentials(agent);
|
||||
|
||||
// Only show credential fields that have system credentials
|
||||
const credentialFieldsWithSystemCreds = useMemo(() => {
|
||||
return systemCredentials.map((item) => ({
|
||||
fieldKey: item.key,
|
||||
schema: item.schema,
|
||||
systemCredential: item.credential,
|
||||
}));
|
||||
}, [systemCredentials]);
|
||||
|
||||
const hasAnySettings = hasHITLBlocks || hasSystemCredentials;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -57,23 +71,59 @@ export function AgentSettingsModal({
|
||||
)}
|
||||
<Dialog.Content>
|
||||
<div className="space-y-6">
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">Require human approval</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause and wait for your review before
|
||||
continuing
|
||||
{hasHITLBlocks && (
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">Require human approval</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause and wait for your review before
|
||||
continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSafeMode || false}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSystemCredentials && (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div>
|
||||
<Text variant="large-semibold">System Credentials</Text>
|
||||
<Text variant="body" className="mt-1 text-muted-foreground">
|
||||
These credentials are managed by AutoGPT and used by the agent
|
||||
to access various services. You can switch to your own
|
||||
credentials if preferred.
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSafeMode || false}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="w-full space-y-4">
|
||||
{credentialFieldsWithSystemCreds.map(
|
||||
({ fieldKey, schema, systemCredential }) => (
|
||||
<SystemCredentialRow
|
||||
key={fieldKey}
|
||||
credentialKey={fieldKey}
|
||||
agentId={agent.id.toString()}
|
||||
schema={schema}
|
||||
systemCredential={systemCredential}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasAnySettings && (
|
||||
<div className="py-6">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
This agent doesn't have any configurable settings.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/molecules/Accordion/Accordion";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
@@ -13,7 +7,6 @@ import {
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||
import { SlidersHorizontalIcon } from "lucide-react";
|
||||
import { APIKeyCredentialsModal } from "./components/APIKeyCredentialsModal/APIKeyCredentialsModal";
|
||||
import { CredentialRow } from "./components/CredentialRow/CredentialRow";
|
||||
import { CredentialsSelect } from "./components/CredentialsSelect/CredentialsSelect";
|
||||
@@ -43,7 +36,7 @@ type Props = {
|
||||
isOptional?: boolean;
|
||||
showTitle?: boolean;
|
||||
variant?: "default" | "node";
|
||||
collapseSystemCredentials?: boolean;
|
||||
allowSystemCredentials?: boolean; // Allow system credentials (for settings only)
|
||||
};
|
||||
|
||||
export function CredentialsInput({
|
||||
@@ -57,7 +50,7 @@ export function CredentialsInput({
|
||||
isOptional = false,
|
||||
showTitle = true,
|
||||
variant = "default",
|
||||
collapseSystemCredentials = false,
|
||||
allowSystemCredentials = false,
|
||||
}: Props) {
|
||||
const hookData = useCredentialsInput({
|
||||
schema,
|
||||
@@ -67,6 +60,7 @@ export function CredentialsInput({
|
||||
onLoaded,
|
||||
readOnly,
|
||||
isOptional,
|
||||
allowSystemCredentials,
|
||||
});
|
||||
|
||||
if (!isLoaded(hookData)) {
|
||||
@@ -80,9 +74,7 @@ export function CredentialsInput({
|
||||
supportsOAuth2,
|
||||
supportsUserPassword,
|
||||
supportsHostScoped,
|
||||
isSystemProvider,
|
||||
userCredentials,
|
||||
systemCredentials,
|
||||
credentialsToShow,
|
||||
oAuthError,
|
||||
isAPICredentialsModalOpen,
|
||||
isUserPasswordCredentialsModalOpen,
|
||||
@@ -98,45 +90,17 @@ export function CredentialsInput({
|
||||
} = hookData;
|
||||
|
||||
const displayName = toDisplayName(provider);
|
||||
const hasCredentialsToShow = credentialsToShow.length > 0;
|
||||
const selectedCredentialIsSystem =
|
||||
selectedCredential && isSystemCredential(selectedCredential);
|
||||
|
||||
const allCredentials = [...userCredentials, ...systemCredentials];
|
||||
|
||||
// When collapseSystemCredentials is true AND provider is a system provider,
|
||||
// collapse ALL credentials (both user and system) under the accordion.
|
||||
// This keeps the provider section clean when platform credits are available.
|
||||
const shouldCollapseAll = collapseSystemCredentials && isSystemProvider;
|
||||
|
||||
// Determine which credentials to show in main section
|
||||
const credentialsToShow = shouldCollapseAll
|
||||
? [] // All credentials go to accordion when provider has system creds
|
||||
: collapseSystemCredentials
|
||||
? userCredentials // No system creds, show user creds normally
|
||||
: allCredentials; // Show all when not collapsing
|
||||
|
||||
const hasCredentialsToShow = credentialsToShow.length > 0;
|
||||
|
||||
// Credentials to show in accordion
|
||||
const credentialsToCollapse = shouldCollapseAll
|
||||
? allCredentials // All credentials collapsed when provider has system creds
|
||||
: collapseSystemCredentials
|
||||
? systemCredentials // Only system creds collapsed
|
||||
: [];
|
||||
|
||||
const hasCredentialsToCollapse = credentialsToCollapse.length > 0;
|
||||
|
||||
// If required and no credential selected, keep accordion open
|
||||
const shouldOpenAccordionByDefault =
|
||||
shouldCollapseAll && !isOptional && !selectedCredential;
|
||||
|
||||
if (readOnly && selectedCredentialIsSystem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("mb-6", className)}>
|
||||
{showTitle && !shouldCollapseAll && (
|
||||
{showTitle && (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Text variant="large-medium">
|
||||
{displayName} credentials
|
||||
@@ -168,19 +132,21 @@ export function CredentialsInput({
|
||||
/>
|
||||
) : (
|
||||
<div className="mb-4 space-y-2">
|
||||
{credentialsToShow.map((credential) => (
|
||||
<CredentialRow
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
onSelect={() => handleCredentialSelect(credential.id)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
{credentialsToShow.map((credential) => {
|
||||
return (
|
||||
<CredentialRow
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
onSelect={() => handleCredentialSelect(credential.id)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!readOnly && !shouldCollapseAll && (
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
@@ -193,8 +159,7 @@ export function CredentialsInput({
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!readOnly &&
|
||||
!shouldCollapseAll && (
|
||||
!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
@@ -207,87 +172,6 @@ export function CredentialsInput({
|
||||
)
|
||||
)}
|
||||
|
||||
{shouldCollapseAll && !readOnly && (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={
|
||||
shouldOpenAccordionByDefault ? "system-credentials" : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem value="system-credentials" className="border-none">
|
||||
<AccordionTrigger className="py-2 text-sm text-muted-foreground hover:no-underline">
|
||||
<div className="flex items-center gap-1">
|
||||
<SlidersHorizontalIcon className="size-4" /> System credentials
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 px-1 pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text variant="large-medium">
|
||||
{displayName} credentials
|
||||
{isOptional && (
|
||||
<span className="ml-1 text-sm font-normal text-gray-500">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
{schema.description && (
|
||||
<InformationTooltip description={schema.description} />
|
||||
)}
|
||||
</div>
|
||||
{credentialsToCollapse.length > 0 && (
|
||||
<CredentialsSelect
|
||||
credentials={credentialsToCollapse}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
selectedCredentials={selectedCredential}
|
||||
onSelectCredential={handleCredentialSelect}
|
||||
onClearCredential={() => onSelectCredential(undefined)}
|
||||
readOnly={readOnly}
|
||||
allowNone={isOptional}
|
||||
variant={variant}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleActionButtonClick}
|
||||
className="w-fit"
|
||||
type="button"
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
{hasCredentialsToCollapse && !shouldCollapseAll && !readOnly && (
|
||||
<Accordion type="single" collapsible className="mt-4">
|
||||
<AccordionItem value="system-credentials" className="border-none">
|
||||
<AccordionTrigger className="py-2 text-sm text-muted-foreground hover:no-underline">
|
||||
System credentials
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2 pt-2">
|
||||
{credentialsToCollapse.map((credential) => (
|
||||
<CredentialRow
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
onSelect={() => handleCredentialSelect(credential.id)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
{!readOnly && (
|
||||
<>
|
||||
{supportsApiKey ? (
|
||||
|
||||
@@ -25,6 +25,7 @@ type Params = {
|
||||
onLoaded?: (loaded: boolean) => void;
|
||||
readOnly?: boolean;
|
||||
isOptional?: boolean;
|
||||
allowSystemCredentials?: boolean; // Allow system credentials (for settings only)
|
||||
};
|
||||
|
||||
export function useCredentialsInput({
|
||||
@@ -35,6 +36,7 @@ export function useCredentialsInput({
|
||||
onLoaded,
|
||||
readOnly = false,
|
||||
isOptional = false,
|
||||
allowSystemCredentials = false,
|
||||
}: Params) {
|
||||
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
||||
useState(false);
|
||||
@@ -85,14 +87,22 @@ export function useCredentialsInput({
|
||||
useEffect(() => {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
const availableCreds = credentials.savedCredentials;
|
||||
const availableCreds = allowSystemCredentials
|
||||
? credentials.savedCredentials
|
||||
: filterSystemCredentials(credentials.savedCredentials);
|
||||
if (
|
||||
selectedCredential &&
|
||||
!availableCreds.some((c) => c.id === selectedCredential.id)
|
||||
) {
|
||||
onSelectCredential(undefined);
|
||||
}
|
||||
}, [credentials, selectedCredential, onSelectCredential, readOnly]);
|
||||
}, [
|
||||
credentials,
|
||||
selectedCredential,
|
||||
onSelectCredential,
|
||||
readOnly,
|
||||
allowSystemCredentials,
|
||||
]);
|
||||
|
||||
// The available credential, if there is only one
|
||||
const singleCredential = useMemo(() => {
|
||||
@@ -100,9 +110,11 @@ export function useCredentialsInput({
|
||||
return null;
|
||||
}
|
||||
|
||||
const credsToUse = filterSystemCredentials(credentials.savedCredentials);
|
||||
const credsToUse = allowSystemCredentials
|
||||
? credentials.savedCredentials
|
||||
: filterSystemCredentials(credentials.savedCredentials);
|
||||
return credsToUse.length === 1 ? credsToUse[0] : null;
|
||||
}, [credentials]);
|
||||
}, [credentials, allowSystemCredentials]);
|
||||
|
||||
// Auto-select the one available credential
|
||||
// Prioritize system credentials if available
|
||||
@@ -224,12 +236,12 @@ export function useCredentialsInput({
|
||||
supportsHostScoped,
|
||||
savedCredentials,
|
||||
oAuthCallback,
|
||||
isSystemProvider,
|
||||
} = credentials;
|
||||
|
||||
// Split credentials into user and system
|
||||
const userCredentials = filterSystemCredentials(savedCredentials);
|
||||
const systemCredentials = getSystemCredentials(savedCredentials);
|
||||
// Filter system credentials unless explicitly allowed (for settings)
|
||||
const filteredCredentials = allowSystemCredentials
|
||||
? savedCredentials
|
||||
: filterSystemCredentials(savedCredentials);
|
||||
|
||||
async function handleOAuthLogin() {
|
||||
setOAuthError(null);
|
||||
@@ -385,10 +397,7 @@ export function useCredentialsInput({
|
||||
supportsOAuth2,
|
||||
supportsUserPassword,
|
||||
supportsHostScoped,
|
||||
isSystemProvider,
|
||||
userCredentials,
|
||||
systemCredentials,
|
||||
allCredentials: savedCredentials,
|
||||
credentialsToShow: filteredCredentials,
|
||||
selectedCredential,
|
||||
oAuthError,
|
||||
isAPICredentialsModalOpen,
|
||||
@@ -403,7 +412,7 @@ export function useCredentialsInput({
|
||||
supportsApiKey,
|
||||
supportsUserPassword,
|
||||
supportsHostScoped,
|
||||
userCredentials.length > 0,
|
||||
filteredCredentials.length > 0,
|
||||
),
|
||||
setAPICredentialsModalOpen,
|
||||
setUserPasswordCredentialsModalOpen,
|
||||
|
||||
@@ -3,6 +3,14 @@ import { Input } from "@/components/atoms/Input/Input";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { useContext, useMemo } from "react";
|
||||
import {
|
||||
NONE_CREDENTIAL_MARKER,
|
||||
useAgentCredentialPreferencesStore,
|
||||
} from "../../../../../stores/agentCredentialPreferencesStore";
|
||||
import {
|
||||
filterSystemCredentials,
|
||||
isSystemCredential,
|
||||
} from "../../../CredentialsInputs/helpers";
|
||||
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
|
||||
import { useRunAgentModalContext } from "../../context";
|
||||
import { ModalSection } from "../ModalSection/ModalSection";
|
||||
@@ -25,44 +33,45 @@ export function ModalRunSection() {
|
||||
} = useRunAgentModalContext();
|
||||
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
const store = useAgentCredentialPreferencesStore();
|
||||
|
||||
const inputFields = Object.entries(agentInputFields || {});
|
||||
|
||||
// Sort credential fields: user credentials first, system credentials at the bottom
|
||||
const sortedCredentialFields = useMemo(() => {
|
||||
// Only show credential fields that have user credentials (NOT system credentials)
|
||||
// System credentials should only be shown in settings, not in run modal
|
||||
const credentialFields = useMemo(() => {
|
||||
if (!allProviders || !agentCredentialsInputFields) return [];
|
||||
|
||||
const entries = Object.entries(agentCredentialsInputFields);
|
||||
return Object.entries(agentCredentialsInputFields).filter(
|
||||
([_key, schema]) => {
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
|
||||
return entries.sort(([_keyA, schemaA], [_keyB, schemaB]) => {
|
||||
const providerNamesA = schemaA.credentials_provider || [];
|
||||
const providerNamesB = schemaB.credentials_provider || [];
|
||||
// Check if any provider has user credentials (NOT system credentials)
|
||||
for (const providerName of providerNames) {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) continue;
|
||||
|
||||
// Check if A has system credentials
|
||||
const aHasSystemCred = providerNamesA.some((providerName: string) => {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) return false;
|
||||
return providerData.savedCredentials.some(
|
||||
(cred: { is_system?: boolean }) => cred.is_system === true,
|
||||
);
|
||||
});
|
||||
const userCreds = filterSystemCredentials(
|
||||
providerData.savedCredentials,
|
||||
);
|
||||
const matchingUserCreds = userCreds.filter((cred: { type: string }) =>
|
||||
supportedTypes.includes(cred.type),
|
||||
);
|
||||
|
||||
// Check if B has system credentials
|
||||
const bHasSystemCred = providerNamesB.some((providerName: string) => {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) return false;
|
||||
return providerData.savedCredentials.some(
|
||||
(cred: { is_system?: boolean }) => cred.is_system === true,
|
||||
);
|
||||
});
|
||||
// If there are user credentials available, show this field
|
||||
if (matchingUserCreds.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// User credentials first, system credentials at the bottom
|
||||
if (aHasSystemCred && !bHasSystemCred) return 1;
|
||||
if (!aHasSystemCred && bHasSystemCred) return -1;
|
||||
return 0;
|
||||
});
|
||||
// Hide the field if only system credentials exist (or no credentials at all)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}, [agentCredentialsInputFields, allProviders]);
|
||||
|
||||
// Get the list of required credentials from the schema
|
||||
const requiredCredentials = new Set(
|
||||
(agent.credentials_input_schema?.required as string[]) || [],
|
||||
);
|
||||
@@ -129,31 +138,119 @@ export function ModalRunSection() {
|
||||
</ModalSection>
|
||||
) : null}
|
||||
|
||||
{sortedCredentialFields.length > 0 ? (
|
||||
{credentialFields.length > 0 ? (
|
||||
<ModalSection
|
||||
title="Task Credentials"
|
||||
subtitle="These are the credentials the agent will use to perform this task"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{sortedCredentialFields.map(([key, inputSubSchema]) => {
|
||||
const selectedCred = inputCredentials?.[key];
|
||||
{credentialFields
|
||||
.map(([key, inputSubSchema]) => {
|
||||
const selectedCred = inputCredentials?.[key];
|
||||
|
||||
return (
|
||||
<CredentialsInput
|
||||
key={key}
|
||||
schema={
|
||||
{ ...inputSubSchema, discriminator: undefined } as any
|
||||
// Check if the selected credential is a system credential
|
||||
// First check the credential object itself, then look it up in providers
|
||||
let isSystemCredSelected = false;
|
||||
if (selectedCred) {
|
||||
// Check if credential object has is_system or title indicates system credential
|
||||
isSystemCredSelected = isSystemCredential(
|
||||
selectedCred as { title?: string; is_system?: boolean },
|
||||
);
|
||||
|
||||
// If not detected by title/is_system, check by looking up in providers
|
||||
if (
|
||||
!isSystemCredSelected &&
|
||||
selectedCred.id &&
|
||||
allProviders
|
||||
) {
|
||||
const providerNames =
|
||||
inputSubSchema.credentials_provider || [];
|
||||
for (const providerName of providerNames) {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) continue;
|
||||
const systemCreds = providerData.savedCredentials.filter(
|
||||
(cred: any) => cred.is_system === true,
|
||||
);
|
||||
if (
|
||||
systemCreds.some(
|
||||
(cred: any) => cred.id === selectedCred.id,
|
||||
)
|
||||
) {
|
||||
isSystemCredSelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCredentials={selectedCred}
|
||||
onSelectCredentials={(value) => {
|
||||
setInputCredentialsValue(key, value);
|
||||
}}
|
||||
siblingInputs={inputValues}
|
||||
isOptional={!requiredCredentials.has(key)}
|
||||
collapseSystemCredentials
|
||||
/>
|
||||
);
|
||||
})}
|
||||
}
|
||||
|
||||
// If a system credential is selected, check if there are user credentials available
|
||||
// If not, hide this field entirely (it will still be used for execution)
|
||||
if (isSystemCredSelected) {
|
||||
const providerNames =
|
||||
inputSubSchema.credentials_provider || [];
|
||||
const supportedTypes = inputSubSchema.credentials_types || [];
|
||||
const hasUserCreds = providerNames.some(
|
||||
(providerName: string) => {
|
||||
const providerData = allProviders?.[providerName];
|
||||
if (!providerData) return false;
|
||||
const userCreds = filterSystemCredentials(
|
||||
providerData.savedCredentials,
|
||||
);
|
||||
return userCreds.some((cred: { type: string }) =>
|
||||
supportedTypes.includes(cred.type),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// If no user credentials available, hide the field completely
|
||||
if (!hasUserCreds) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If a system credential is selected but user creds exist, don't show it in the UI
|
||||
// (it will still be used for execution, but user can select a user credential instead)
|
||||
const credToDisplay = isSystemCredSelected
|
||||
? undefined
|
||||
: selectedCred;
|
||||
|
||||
return (
|
||||
<CredentialsInput
|
||||
key={key}
|
||||
schema={
|
||||
{ ...inputSubSchema, discriminator: undefined } as any
|
||||
}
|
||||
selectedCredentials={credToDisplay}
|
||||
onSelectCredentials={(value) => {
|
||||
// When user selects a credential, update the state and save to preferences
|
||||
setInputCredentialsValue(key, value);
|
||||
// Save to preferences store
|
||||
if (value === undefined) {
|
||||
store.setCredentialPreference(
|
||||
agent.id.toString(),
|
||||
key,
|
||||
NONE_CREDENTIAL_MARKER,
|
||||
);
|
||||
} else if (value === null) {
|
||||
store.setCredentialPreference(
|
||||
agent.id.toString(),
|
||||
key,
|
||||
null,
|
||||
);
|
||||
} else {
|
||||
store.setCredentialPreference(
|
||||
agent.id.toString(),
|
||||
key,
|
||||
value,
|
||||
);
|
||||
}
|
||||
}}
|
||||
siblingInputs={inputValues}
|
||||
isOptional={!requiredCredentials.has(key)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</div>
|
||||
</ModalSection>
|
||||
) : null}
|
||||
|
||||
@@ -22,7 +22,14 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getSystemCredentials } from "../CredentialsInputs/helpers";
|
||||
import {
|
||||
NONE_CREDENTIAL_MARKER,
|
||||
useAgentCredentialPreferencesStore,
|
||||
} from "../../../stores/agentCredentialPreferencesStore";
|
||||
import {
|
||||
filterSystemCredentials,
|
||||
getSystemCredentials,
|
||||
} from "../CredentialsInputs/helpers";
|
||||
import { showExecutionErrorToast } from "./errorHelpers";
|
||||
|
||||
export type RunVariant =
|
||||
@@ -70,27 +77,47 @@ export function useAgentRunModal(
|
||||
}, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]);
|
||||
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
const store = useAgentCredentialPreferencesStore();
|
||||
|
||||
// Initialize credentials with default system credentials
|
||||
// Initialize credentials from saved preferences or default system credentials
|
||||
// This ensures credentials are used even when the field is not displayed
|
||||
useEffect(() => {
|
||||
if (!allProviders || !agent.credentials_input_schema?.properties) return;
|
||||
if (callbacks?.initialInputCredentials) {
|
||||
hasInitializedSystemCreds.current = true;
|
||||
return;
|
||||
return; // Don't override if initial credentials provided
|
||||
}
|
||||
if (hasInitializedSystemCreds.current) return;
|
||||
if (hasInitializedSystemCreds.current) return; // Already initialized
|
||||
|
||||
const properties = agent.credentials_input_schema.properties as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
// Use functional update to get current state and avoid stale closures
|
||||
setInputCredentials((currentCreds) => {
|
||||
const credsToAdd: Record<string, any> = {};
|
||||
|
||||
for (const [key, schema] of Object.entries(properties)) {
|
||||
// Skip if already set
|
||||
if (currentCreds[key]) continue;
|
||||
|
||||
// First, check if user has a saved preference
|
||||
const savedPreference = store.getCredentialPreference(
|
||||
agent.id.toString(),
|
||||
key,
|
||||
);
|
||||
// Check if "None" was explicitly selected (special marker)
|
||||
if (savedPreference === NONE_CREDENTIAL_MARKER) {
|
||||
// User explicitly selected "None" - don't add any credential
|
||||
continue;
|
||||
}
|
||||
if (savedPreference) {
|
||||
credsToAdd[key] = savedPreference;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, find default system credentials for this field
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
const requiredScopes = schema.credentials_scopes;
|
||||
@@ -105,6 +132,7 @@ export function useAgentRunModal(
|
||||
const matchingSystemCreds = systemCreds.filter((cred) => {
|
||||
if (!supportedTypes.includes(cred.type)) return false;
|
||||
|
||||
// For OAuth2 credentials, check scopes
|
||||
if (
|
||||
cred.type === "oauth2" &&
|
||||
requiredScopes &&
|
||||
@@ -120,6 +148,7 @@ export function useAgentRunModal(
|
||||
return true;
|
||||
});
|
||||
|
||||
// If there's exactly one system credential, use it as default
|
||||
if (matchingSystemCreds.length === 1) {
|
||||
const systemCred = matchingSystemCreds[0];
|
||||
credsToAdd[key] = {
|
||||
@@ -128,11 +157,12 @@ export function useAgentRunModal(
|
||||
provider: providerName,
|
||||
title: systemCred.title,
|
||||
};
|
||||
break;
|
||||
break; // Use first matching provider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only update if we found credentials to add
|
||||
if (Object.keys(credsToAdd).length > 0) {
|
||||
hasInitializedSystemCreds.current = true;
|
||||
return {
|
||||
@@ -141,11 +171,95 @@ export function useAgentRunModal(
|
||||
};
|
||||
}
|
||||
|
||||
return currentCreds;
|
||||
return currentCreds; // No changes
|
||||
});
|
||||
}, [
|
||||
allProviders,
|
||||
agent.credentials_input_schema,
|
||||
agent.id,
|
||||
store,
|
||||
callbacks?.initialInputCredentials,
|
||||
]);
|
||||
|
||||
// Sync credentials with preferences store when modal opens
|
||||
useEffect(() => {
|
||||
if (!isOpen || !allProviders || !agent.credentials_input_schema?.properties)
|
||||
return;
|
||||
if (callbacks?.initialInputCredentials) return; // Don't override if initial credentials provided
|
||||
|
||||
const properties = agent.credentials_input_schema.properties as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
setInputCredentials((currentCreds) => {
|
||||
const updatedCreds: Record<string, any> = { ...currentCreds };
|
||||
|
||||
for (const [key, schema] of Object.entries(properties)) {
|
||||
const savedPreference = store.getCredentialPreference(
|
||||
agent.id.toString(),
|
||||
key,
|
||||
);
|
||||
|
||||
if (savedPreference === NONE_CREDENTIAL_MARKER) {
|
||||
// User explicitly selected "None" - remove from credentials
|
||||
delete updatedCreds[key];
|
||||
} else if (savedPreference) {
|
||||
// User has a saved preference - use it
|
||||
updatedCreds[key] = savedPreference;
|
||||
} else if (!updatedCreds[key]) {
|
||||
// No preference and no current credential - try to find default system credential
|
||||
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 = new Set(requiredScopes).isSubsetOf(
|
||||
grantedScopes,
|
||||
);
|
||||
if (!hasAllRequiredScopes) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingSystemCreds.length === 1) {
|
||||
const systemCred = matchingSystemCreds[0];
|
||||
updatedCreds[key] = {
|
||||
id: systemCred.id,
|
||||
type: systemCred.type,
|
||||
provider: providerName,
|
||||
title: systemCred.title,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCreds;
|
||||
});
|
||||
}, [
|
||||
isOpen,
|
||||
agent.id,
|
||||
agent.credentials_input_schema,
|
||||
allProviders,
|
||||
store,
|
||||
callbacks?.initialInputCredentials,
|
||||
]);
|
||||
|
||||
@@ -162,6 +276,7 @@ export function useAgentRunModal(
|
||||
toast({
|
||||
title: "Agent execution started",
|
||||
});
|
||||
// Invalidate runs list for this graph
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
|
||||
});
|
||||
@@ -258,23 +373,83 @@ 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[]) || [],
|
||||
);
|
||||
|
||||
const missing = [...requiredCredentials].filter((key) => {
|
||||
// Filter out credential fields that only have system credentials available
|
||||
// System credentials should not be required in the run modal
|
||||
// Also check if user has a saved preference (including NONE_MARKER)
|
||||
const requiredCredentialsToCheck = [...requiredCredentials].filter(
|
||||
(key) => {
|
||||
// Check if user has a saved preference first
|
||||
const savedPreference = store.getCredentialPreference(
|
||||
agent.id.toString(),
|
||||
key,
|
||||
);
|
||||
// If "None" was explicitly selected, don't require it
|
||||
if (savedPreference === NONE_CREDENTIAL_MARKER) {
|
||||
return false;
|
||||
}
|
||||
// If user has a saved preference, it should be checked
|
||||
if (savedPreference) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const schema = agentCredentialsInputFields[key];
|
||||
if (!schema || !allProviders) return true; // If we can't check, include it
|
||||
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
|
||||
// Check if any provider has non-system credentials available
|
||||
for (const providerName of providerNames) {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) continue;
|
||||
|
||||
const userCreds = filterSystemCredentials(
|
||||
providerData.savedCredentials,
|
||||
);
|
||||
const matchingUserCreds = userCreds.filter((cred) =>
|
||||
supportedTypes.includes(cred.type),
|
||||
);
|
||||
|
||||
// If there are user credentials available, this field should be checked
|
||||
if (matchingUserCreds.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If only system credentials are available, exclude from required check
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
// 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 = requiredCredentialsToCheck.filter((key) => {
|
||||
const cred = inputCredentials[key];
|
||||
return !cred || !cred.id;
|
||||
});
|
||||
|
||||
return [missing.length === 0, missing];
|
||||
}, [agent.credentials_input_schema, inputCredentials]);
|
||||
}, [
|
||||
agent.credentials_input_schema,
|
||||
agentCredentialsInputFields,
|
||||
inputCredentials,
|
||||
allProviders,
|
||||
agent.id,
|
||||
store,
|
||||
]);
|
||||
|
||||
const credentialsRequired = useMemo(
|
||||
() => Object.keys(agentCredentialsInputFields || {}).length > 0,
|
||||
[agentCredentialsInputFields],
|
||||
);
|
||||
|
||||
// Final readiness flag combining inputs + credentials when credentials are shown
|
||||
const allRequiredInputsAreSet = useMemo(
|
||||
() =>
|
||||
allRequiredInputsAreSetRaw &&
|
||||
@@ -313,6 +488,7 @@ export function useAgentRunModal(
|
||||
defaultRunType === "automatic-trigger" ||
|
||||
defaultRunType === "manual-trigger"
|
||||
) {
|
||||
// Setup trigger
|
||||
if (!presetName.trim()) {
|
||||
toast({
|
||||
title: "⚠️ Trigger name required",
|
||||
@@ -333,6 +509,9 @@ 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),
|
||||
);
|
||||
@@ -366,24 +545,41 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useAgentMissingCredentials } from "../../hooks/useAgentMissingCredentials";
|
||||
import { RunAgentModal } from "../modals/RunAgentModal/RunAgentModal";
|
||||
import { RunDetailCard } from "../selected-views/RunDetailCard/RunDetailCard";
|
||||
import { EmptyTasksIllustration } from "./EmptyTasksIllustration";
|
||||
@@ -44,6 +45,7 @@ export function EmptyTasks({
|
||||
const [isDeletingAgent, setIsDeletingAgent] = useState(false);
|
||||
|
||||
const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent();
|
||||
const { hasMissingCredentials } = useAgentMissingCredentials(agent);
|
||||
|
||||
async function handleDeleteAgent() {
|
||||
if (!agent.id) return;
|
||||
@@ -124,6 +126,7 @@ export function EmptyTasks({
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="inline-flex w-[19.75rem]"
|
||||
disabled={hasMissingCredentials}
|
||||
>
|
||||
Setup your task
|
||||
</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||
import { SelectedViewLayout } from "../SelectedViewLayout";
|
||||
import { SystemCredentialsSection } from "./components/SystemCredentialsSection";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
@@ -16,6 +17,10 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||
useAgentSafeMode(agent);
|
||||
|
||||
const hasCredentialsSchema =
|
||||
agent.credentials_input_schema &&
|
||||
Object.keys(agent.credentials_input_schema.properties || {}).length > 0;
|
||||
|
||||
return (
|
||||
<SelectedViewLayout agent={agent}>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -34,7 +39,7 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
</div>
|
||||
|
||||
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
|
||||
{hasHITLBlocks ? (
|
||||
{hasHITLBlocks && (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
@@ -52,7 +57,11 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{hasCredentialsSchema && <SystemCredentialsSection agent={agent} />}
|
||||
|
||||
{!hasHITLBlocks && !hasCredentialsSchema && (
|
||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
This agent doesn't have any configurable settings.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CredentialsMetaResponse } from "@/lib/autogpt-server-api/types";
|
||||
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CredentialsInput } from "../../../../components/modals/CredentialsInputs/CredentialsInputs";
|
||||
import {
|
||||
NONE_CREDENTIAL_MARKER,
|
||||
useAgentCredentialPreferencesStore,
|
||||
} from "../../../../stores/agentCredentialPreferencesStore";
|
||||
|
||||
interface Props {
|
||||
credentialKey: string;
|
||||
agentId: string;
|
||||
schema: any;
|
||||
systemCredential: CredentialsMetaResponse;
|
||||
}
|
||||
|
||||
export function SystemCredentialRow({
|
||||
credentialKey,
|
||||
agentId,
|
||||
schema,
|
||||
systemCredential,
|
||||
}: Props) {
|
||||
const store = useAgentCredentialPreferencesStore();
|
||||
|
||||
// Initialize with saved preference or default to system credential
|
||||
const savedPreference = store.getCredentialPreference(agentId, credentialKey);
|
||||
const defaultCredential = {
|
||||
id: systemCredential.id,
|
||||
type: systemCredential.type,
|
||||
provider: systemCredential.provider,
|
||||
title: systemCredential.title,
|
||||
};
|
||||
|
||||
// If saved preference is the NONE marker, use undefined (which CredentialsInput interprets as "None")
|
||||
// Otherwise use saved preference or default
|
||||
const [selectedCredential, setSelectedCredential] = useState<any>(
|
||||
savedPreference === NONE_CREDENTIAL_MARKER
|
||||
? undefined
|
||||
: savedPreference || defaultCredential,
|
||||
);
|
||||
|
||||
// Update when preference changes externally
|
||||
useEffect(() => {
|
||||
const preference = store.getCredentialPreference(agentId, credentialKey);
|
||||
if (preference === NONE_CREDENTIAL_MARKER) {
|
||||
setSelectedCredential(undefined);
|
||||
} else if (preference) {
|
||||
setSelectedCredential(preference);
|
||||
} else {
|
||||
setSelectedCredential(defaultCredential);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [credentialKey, agentId]);
|
||||
|
||||
const providerName = schema.credentials_provider?.[0] || "";
|
||||
const displayName = toDisplayName(providerName);
|
||||
|
||||
function handleSelectCredentials(value: any) {
|
||||
setSelectedCredential(value);
|
||||
// Save preference:
|
||||
// - undefined = explicitly selected "None" (save NONE_CREDENTIAL_MARKER)
|
||||
// - null = use default system credential (fallback behavior, save null)
|
||||
// - credential object = use this specific credential
|
||||
if (value === undefined) {
|
||||
// User explicitly selected "None" - save special marker
|
||||
store.setCredentialPreference(
|
||||
agentId,
|
||||
credentialKey,
|
||||
NONE_CREDENTIAL_MARKER,
|
||||
);
|
||||
} else if (value === null) {
|
||||
// User cleared selection - use default system credential
|
||||
store.setCredentialPreference(agentId, credentialKey, null);
|
||||
} else {
|
||||
// User selected a credential
|
||||
store.setCredentialPreference(agentId, credentialKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-100 bg-zinc-50/50 px-4 pb-2 pt-4">
|
||||
<Text variant="body-medium" className="mb-2 ml-2">
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
<CredentialsInput
|
||||
schema={{ ...schema, discriminator: undefined }}
|
||||
selectedCredentials={selectedCredential}
|
||||
onSelectCredentials={handleSelectCredentials}
|
||||
showTitle={false}
|
||||
isOptional
|
||||
allowSystemCredentials={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useAgentSystemCredentials } from "../../../../hooks/useAgentSystemCredentials";
|
||||
import { SystemCredentialRow } from "./SystemCredentialRow";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
}
|
||||
|
||||
export function SystemCredentialsSection({ agent }: Props) {
|
||||
const { hasSystemCredentials, systemCredentials, isLoading } =
|
||||
useAgentSystemCredentials(agent);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<Text variant="large-semibold">System Credentials</Text>
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
Loading credentials...
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasSystemCredentials) return null;
|
||||
|
||||
// Group by credential field key (from schema) to show one row per field
|
||||
const credentialsByField = systemCredentials.reduce(
|
||||
(acc, item) => {
|
||||
if (!acc[item.key]) {
|
||||
acc[item.key] = item;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, (typeof systemCredentials)[0]>,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div>
|
||||
<Text variant="large-semibold">System Credentials</Text>
|
||||
<Text variant="body" className="mt-1 text-muted-foreground">
|
||||
These credentials are managed by AutoGPT and used by the agent to
|
||||
access various services. You can switch to your own credentials if
|
||||
preferred.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
{Object.entries(credentialsByField).map(([fieldKey, item]) => (
|
||||
<SystemCredentialRow
|
||||
key={fieldKey}
|
||||
credentialKey={fieldKey}
|
||||
agentId={agent.id.toString()}
|
||||
schema={item.schema}
|
||||
systemCredential={item.credential}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||
import { storage } from "@/services/storage/local-storage";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
// Special marker to indicate "None" was explicitly selected
|
||||
export const NONE_CREDENTIAL_MARKER = { __none__: true } as const;
|
||||
|
||||
type AgentCredentialPreferences = Record<
|
||||
string,
|
||||
CredentialsMetaInput | null | typeof NONE_CREDENTIAL_MARKER
|
||||
>;
|
||||
|
||||
const STORAGE_KEY_PREFIX = "agent_credential_prefs_";
|
||||
|
||||
function getStorageKey(agentId: string): string {
|
||||
return `${STORAGE_KEY_PREFIX}${agentId}`;
|
||||
}
|
||||
|
||||
function loadPreferences(agentId: string): AgentCredentialPreferences {
|
||||
const key = getStorageKey(agentId);
|
||||
const stored = storage.get(key as any);
|
||||
if (!stored) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
// Convert serialized NONE markers back to the constant
|
||||
const result: AgentCredentialPreferences = {};
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
"__none__" in value &&
|
||||
(value as any).__none__ === true
|
||||
) {
|
||||
result[key] = NONE_CREDENTIAL_MARKER;
|
||||
} else {
|
||||
result[key] = value as CredentialsMetaInput | null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function savePreferences(
|
||||
agentId: string,
|
||||
preferences: AgentCredentialPreferences,
|
||||
): void {
|
||||
const key = getStorageKey(agentId);
|
||||
storage.set(key as any, JSON.stringify(preferences));
|
||||
}
|
||||
|
||||
export function useAgentCredentialPreferences(agentId: string) {
|
||||
const [preferences, setPreferences] = useState<AgentCredentialPreferences>(
|
||||
() => loadPreferences(agentId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loaded = loadPreferences(agentId);
|
||||
setPreferences(loaded);
|
||||
}, [agentId]);
|
||||
|
||||
const setCredentialPreference = useCallback(
|
||||
(
|
||||
credentialKey: string,
|
||||
credential: CredentialsMetaInput | null | typeof NONE_CREDENTIAL_MARKER,
|
||||
) => {
|
||||
setPreferences((prev) => {
|
||||
const updated = {
|
||||
...prev,
|
||||
[credentialKey]: credential,
|
||||
};
|
||||
savePreferences(agentId, updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[agentId],
|
||||
);
|
||||
|
||||
const getCredentialPreference = useCallback(
|
||||
(
|
||||
credentialKey: string,
|
||||
): CredentialsMetaInput | null | typeof NONE_CREDENTIAL_MARKER => {
|
||||
return preferences[credentialKey] ?? null;
|
||||
},
|
||||
[preferences],
|
||||
);
|
||||
|
||||
const clearPreference = useCallback(
|
||||
(credentialKey: string) => {
|
||||
setPreferences((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[credentialKey];
|
||||
savePreferences(agentId, updated);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[agentId],
|
||||
);
|
||||
|
||||
return {
|
||||
preferences,
|
||||
setCredentialPreference,
|
||||
getCredentialPreference,
|
||||
clearPreference,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { getSystemCredentials } from "../components/modals/CredentialsInputs/helpers";
|
||||
|
||||
/**
|
||||
* Hook to check if an agent is missing required SYSTEM credentials.
|
||||
* This is only used to block "New Task" buttons.
|
||||
* User credential validation is handled separately in RunAgentModal.
|
||||
*/
|
||||
export function useAgentMissingCredentials(
|
||||
agent: LibraryAgent | null | undefined,
|
||||
) {
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
const result = useMemo(() => {
|
||||
if (
|
||||
!agent ||
|
||||
!agent.id ||
|
||||
!allProviders ||
|
||||
!agent.credentials_input_schema?.properties
|
||||
) {
|
||||
return {
|
||||
hasMissingCredentials: false,
|
||||
missingCredentials: [],
|
||||
isLoading: !allProviders || !agent,
|
||||
};
|
||||
}
|
||||
|
||||
const properties = agent.credentials_input_schema.properties as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
const requiredCredentials = new Set(
|
||||
(agent.credentials_input_schema.required as string[]) || [],
|
||||
);
|
||||
|
||||
const missingCredentials: Array<{
|
||||
key: string;
|
||||
providerDisplayName: string;
|
||||
}> = [];
|
||||
|
||||
for (const [key, schema] of Object.entries(properties)) {
|
||||
const isRequired = requiredCredentials.has(key);
|
||||
if (!isRequired) continue; // Only check required credentials
|
||||
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
const requiredScopes = schema.credentials_scopes;
|
||||
|
||||
let hasSystemCredential = false;
|
||||
|
||||
// Check if any provider has a system credential available
|
||||
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 = new Set(requiredScopes).isSubsetOf(
|
||||
grantedScopes,
|
||||
);
|
||||
if (!hasAllRequiredScopes) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// If there's a system credential available, it's not missing
|
||||
if (matchingSystemCreds.length > 0) {
|
||||
hasSystemCredential = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system credential available, mark as missing
|
||||
if (!hasSystemCredential) {
|
||||
const providerName = providerNames[0] || "";
|
||||
missingCredentials.push({
|
||||
key,
|
||||
providerDisplayName: toDisplayName(providerName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasMissingCredentials: missingCredentials.length > 0,
|
||||
missingCredentials,
|
||||
isLoading: false,
|
||||
};
|
||||
}, [allProviders, agent?.credentials_input_schema, agent?.id]);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { CredentialsMetaResponse } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
CredentialsProviderData,
|
||||
CredentialsProvidersContext,
|
||||
} from "@/providers/agent-credentials/credentials-provider";
|
||||
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||
import { useContext, useMemo } from "react";
|
||||
import {
|
||||
filterSystemCredentials,
|
||||
getSystemCredentials,
|
||||
} from "../components/modals/CredentialsInputs/helpers";
|
||||
|
||||
interface SystemCredentialInfo {
|
||||
key: string;
|
||||
provider: string;
|
||||
schema: any;
|
||||
credential: CredentialsMetaResponse;
|
||||
}
|
||||
|
||||
interface MissingCredentialInfo {
|
||||
key: string;
|
||||
provider: string;
|
||||
providerDisplayName: string;
|
||||
}
|
||||
|
||||
interface UseAgentSystemCredentialsResult {
|
||||
hasSystemCredentials: boolean;
|
||||
systemCredentials: SystemCredentialInfo[];
|
||||
hasMissingSystemCredentials: boolean;
|
||||
missingSystemCredentials: MissingCredentialInfo[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function useAgentSystemCredentials(
|
||||
agent: LibraryAgent,
|
||||
): UseAgentSystemCredentialsResult {
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
const result = useMemo(() => {
|
||||
const empty = {
|
||||
hasSystemCredentials: false,
|
||||
systemCredentials: [],
|
||||
hasMissingSystemCredentials: false,
|
||||
missingSystemCredentials: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
if (!agent.credentials_input_schema?.properties) return empty;
|
||||
|
||||
if (!allProviders) return { ...empty, isLoading: true };
|
||||
|
||||
const properties = agent.credentials_input_schema.properties as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
const requiredCredentials = new Set(
|
||||
(agent.credentials_input_schema.required as string[]) || [],
|
||||
);
|
||||
const systemCredentials: SystemCredentialInfo[] = [];
|
||||
const missingSystemCredentials: MissingCredentialInfo[] = [];
|
||||
|
||||
for (const [key, schema] of Object.entries(properties)) {
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const isRequired = requiredCredentials.has(key);
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
|
||||
for (const providerName of providerNames) {
|
||||
const providerData: CredentialsProviderData | undefined =
|
||||
allProviders[providerName];
|
||||
|
||||
if (!providerData) {
|
||||
// Provider not loaded yet - don't mark as missing, wait for load
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for system credentials - now backend always returns them with is_system: true
|
||||
const systemCreds = getSystemCredentials(providerData.savedCredentials);
|
||||
const userCreds = filterSystemCredentials(
|
||||
providerData.savedCredentials,
|
||||
);
|
||||
|
||||
const matchingSystemCreds = systemCreds.filter((cred) =>
|
||||
supportedTypes.includes(cred.type),
|
||||
);
|
||||
const matchingUserCreds = userCreds.filter((cred) =>
|
||||
supportedTypes.includes(cred.type),
|
||||
);
|
||||
|
||||
// Add system credentials if they exist (even if not configured, backend returns them)
|
||||
for (const cred of matchingSystemCreds) {
|
||||
systemCredentials.push({
|
||||
key,
|
||||
provider: providerName,
|
||||
schema,
|
||||
credential: cred,
|
||||
});
|
||||
}
|
||||
|
||||
// Only mark as missing if it's required AND there are NO credentials available
|
||||
// (neither system nor user). This is a true "missing" state.
|
||||
// Note: We don't block based on this anymore since the run modal
|
||||
// has its own validation (allRequiredInputsAreSet)
|
||||
if (
|
||||
isRequired &&
|
||||
matchingSystemCreds.length === 0 &&
|
||||
matchingUserCreds.length === 0
|
||||
) {
|
||||
missingSystemCredentials.push({
|
||||
key,
|
||||
provider: providerName,
|
||||
providerDisplayName: toDisplayName(providerName),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasSystemCredentials: systemCredentials.length > 0,
|
||||
systemCredentials,
|
||||
hasMissingSystemCredentials: missingSystemCredentials.length > 0,
|
||||
missingSystemCredentials,
|
||||
isLoading: false,
|
||||
};
|
||||
}, [agent.credentials_input_schema, allProviders]);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||
import { storage } from "@/services/storage/local-storage";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
// Special marker to indicate "None" was explicitly selected
|
||||
export const NONE_CREDENTIAL_MARKER = { __none__: true } as const;
|
||||
|
||||
type CredentialPreference =
|
||||
| CredentialsMetaInput
|
||||
| null
|
||||
| typeof NONE_CREDENTIAL_MARKER;
|
||||
|
||||
type AgentCredentialPreferences = Record<string, CredentialPreference>;
|
||||
|
||||
interface AgentCredentialPreferencesStore {
|
||||
preferences: Record<string, AgentCredentialPreferences>; // agentId -> preferences
|
||||
setCredentialPreference: (
|
||||
agentId: string,
|
||||
credentialKey: string,
|
||||
credential: CredentialPreference,
|
||||
) => void;
|
||||
getCredentialPreference: (
|
||||
agentId: string,
|
||||
credentialKey: string,
|
||||
) => CredentialPreference;
|
||||
clearPreference: (agentId: string, credentialKey: string) => void;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "agent_credential_preferences";
|
||||
|
||||
// Custom storage adapter for localStorage
|
||||
const customStorage = {
|
||||
getItem: (name: string): string | null => {
|
||||
return storage.get(name as any) || null;
|
||||
},
|
||||
setItem: (name: string, value: string): void => {
|
||||
storage.set(name as any, value);
|
||||
},
|
||||
removeItem: (name: string): void => {
|
||||
storage.clean(name as any);
|
||||
},
|
||||
};
|
||||
|
||||
export const useAgentCredentialPreferencesStore =
|
||||
create<AgentCredentialPreferencesStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
preferences: {},
|
||||
|
||||
setCredentialPreference: (agentId, credentialKey, credential) => {
|
||||
set((state) => {
|
||||
const agentPrefs = state.preferences[agentId] || {};
|
||||
const updated = {
|
||||
...state.preferences,
|
||||
[agentId]: {
|
||||
...agentPrefs,
|
||||
[credentialKey]: credential,
|
||||
},
|
||||
};
|
||||
return { preferences: updated };
|
||||
});
|
||||
},
|
||||
|
||||
getCredentialPreference: (agentId, credentialKey) => {
|
||||
const state = get();
|
||||
const pref = state.preferences[agentId]?.[credentialKey];
|
||||
// Convert serialized NONE marker back to constant
|
||||
if (
|
||||
pref &&
|
||||
typeof pref === "object" &&
|
||||
"__none__" in pref &&
|
||||
(pref as any).__none__ === true &&
|
||||
pref !== NONE_CREDENTIAL_MARKER
|
||||
) {
|
||||
return NONE_CREDENTIAL_MARKER;
|
||||
}
|
||||
return pref ?? null;
|
||||
},
|
||||
|
||||
clearPreference: (agentId, credentialKey) => {
|
||||
set((state) => {
|
||||
const agentPrefs = state.preferences[agentId] || {};
|
||||
const updated = { ...agentPrefs };
|
||||
delete updated[credentialKey];
|
||||
return {
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
[agentId]: updated,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
storage: createJSONStorage(() => customStorage),
|
||||
// Transform on rehydrate to convert NONE markers
|
||||
onRehydrateStorage: () => (state, error) => {
|
||||
if (error || !state) {
|
||||
console.error("Failed to rehydrate credential preferences:", error);
|
||||
return;
|
||||
}
|
||||
// Convert serialized NONE markers back to constant
|
||||
const converted: Record<string, AgentCredentialPreferences> = {};
|
||||
for (const [agentId, prefs] of Object.entries(
|
||||
state.preferences || {},
|
||||
)) {
|
||||
const convertedPrefs: AgentCredentialPreferences = {};
|
||||
for (const [key, value] of Object.entries(prefs)) {
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
"__none__" in value &&
|
||||
(value as any).__none__ === true &&
|
||||
value !== NONE_CREDENTIAL_MARKER
|
||||
) {
|
||||
convertedPrefs[key] = NONE_CREDENTIAL_MARKER;
|
||||
} else {
|
||||
convertedPrefs[key] = value as CredentialPreference;
|
||||
}
|
||||
}
|
||||
converted[agentId] = convertedPrefs;
|
||||
}
|
||||
// Update state with converted preferences
|
||||
if (
|
||||
Object.keys(converted).length > 0 ||
|
||||
Object.keys(state.preferences || {}).length > 0
|
||||
) {
|
||||
state.preferences = converted;
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -2775,28 +2775,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"],
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import type { Meta } from "@storybook/nextjs";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./Accordion";
|
||||
|
||||
const meta: Meta<typeof Accordion> = {
|
||||
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
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
\`\`\`
|
||||
|
||||
### 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 (
|
||||
<Accordion type="single" collapsible className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It comes with default styles that match your design system.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Is it animated?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It's animated by default with smooth expand/collapse
|
||||
transitions.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function Multiple() {
|
||||
return (
|
||||
<Accordion type="multiple" className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>First section</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Multiple items can be open at the same time when type is set to
|
||||
"multiple".
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Second section</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Try opening this one while the first is still open.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Third section</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
All three can be open simultaneously.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function DefaultOpen() {
|
||||
return (
|
||||
<Accordion type="single" collapsible defaultValue="item-2" className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Closed by default</AccordionTrigger>
|
||||
<AccordionContent>This item starts closed.</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Open by default</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
This item starts open because defaultValue is set to
|
||||
"item-2".
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Also closed</AccordionTrigger>
|
||||
<AccordionContent>This item also starts closed.</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithDisabledItem() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Available item</AccordionTrigger>
|
||||
<AccordionContent>This item can be toggled.</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2" disabled>
|
||||
<AccordionTrigger>Disabled item</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
This content cannot be accessed because the item is disabled.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Another available item</AccordionTrigger>
|
||||
<AccordionContent>This item can also be toggled.</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomStyled() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-96">
|
||||
<AccordionItem value="item-1" className="border-none">
|
||||
<AccordionTrigger className="rounded-lg bg-zinc-100 px-4 hover:bg-zinc-200 hover:no-underline">
|
||||
Custom styled trigger
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2">
|
||||
You can customize the styling using className props.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2" className="mt-2 border-none">
|
||||
<AccordionTrigger className="rounded-lg bg-blue-50 px-4 text-blue-700 hover:bg-blue-100 hover:no-underline">
|
||||
Blue themed
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2 text-blue-600">
|
||||
Each item can have different styles.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200 dark:text-neutral-400" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -352,10 +352,6 @@ export default class BackendAPI {
|
||||
return this._get("/integrations/providers");
|
||||
}
|
||||
|
||||
listSystemProviders(): Promise<string[]> {
|
||||
return this._get("/integrations/providers/system");
|
||||
}
|
||||
|
||||
listCredentials(provider?: string): Promise<CredentialsMetaResponse[]> {
|
||||
return this._get(
|
||||
provider
|
||||
|
||||
@@ -32,8 +32,6 @@ export type CredentialsProviderData = {
|
||||
provider: CredentialsProviderName;
|
||||
providerName: string;
|
||||
savedCredentials: CredentialsMetaResponse[];
|
||||
/** Whether this provider has platform credits available (system credentials) */
|
||||
isSystemProvider: boolean;
|
||||
oAuthCallback: (
|
||||
code: string,
|
||||
state_token: string,
|
||||
@@ -70,13 +68,12 @@ export default function CredentialsProvider({
|
||||
const [providers, setProviders] =
|
||||
useState<CredentialsProvidersContextType | null>(null);
|
||||
const [providerNames, setProviderNames] = useState<string[]>([]);
|
||||
const [systemProviders, setSystemProviders] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const { isLoggedIn } = useSupabase();
|
||||
const api = useBackendAPI();
|
||||
const onFailToast = useToastOnFail();
|
||||
|
||||
console.log("providers", providers);
|
||||
|
||||
const addCredentials = useCallback(
|
||||
(
|
||||
provider: CredentialsProviderName,
|
||||
@@ -246,32 +243,27 @@ export default function CredentialsProvider({
|
||||
setProviders((prev) => ({
|
||||
...prev,
|
||||
...Object.fromEntries(
|
||||
providerNames.map((provider) => {
|
||||
const providerCredentials = credentialsByProvider[provider] ?? [];
|
||||
|
||||
return [
|
||||
providerNames.map((provider) => [
|
||||
provider,
|
||||
{
|
||||
provider,
|
||||
{
|
||||
provider,
|
||||
providerName: toDisplayName(provider as string),
|
||||
savedCredentials: providerCredentials,
|
||||
isSystemProvider: systemProviders.has(provider),
|
||||
oAuthCallback: (code: string, state_token: string) =>
|
||||
oAuthCallback(provider, code, state_token),
|
||||
createAPIKeyCredentials: (
|
||||
credentials: APIKeyCredentialsCreatable,
|
||||
) => createAPIKeyCredentials(provider, credentials),
|
||||
createUserPasswordCredentials: (
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
) => createUserPasswordCredentials(provider, credentials),
|
||||
createHostScopedCredentials: (
|
||||
credentials: HostScopedCredentialsCreatable,
|
||||
) => createHostScopedCredentials(provider, credentials),
|
||||
deleteCredentials: (id: string, force: boolean = false) =>
|
||||
deleteCredentials(provider, id, force),
|
||||
} satisfies CredentialsProviderData,
|
||||
];
|
||||
}),
|
||||
providerName: toDisplayName(provider as string),
|
||||
savedCredentials: credentialsByProvider[provider] ?? [],
|
||||
oAuthCallback: (code: string, state_token: string) =>
|
||||
oAuthCallback(provider, code, state_token),
|
||||
createAPIKeyCredentials: (
|
||||
credentials: APIKeyCredentialsCreatable,
|
||||
) => createAPIKeyCredentials(provider, credentials),
|
||||
createUserPasswordCredentials: (
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
) => createUserPasswordCredentials(provider, credentials),
|
||||
createHostScopedCredentials: (
|
||||
credentials: HostScopedCredentialsCreatable,
|
||||
) => createHostScopedCredentials(provider, credentials),
|
||||
deleteCredentials: (id: string, force: boolean = false) =>
|
||||
deleteCredentials(provider, id, force),
|
||||
} satisfies CredentialsProviderData,
|
||||
]),
|
||||
),
|
||||
}));
|
||||
})
|
||||
@@ -280,7 +272,6 @@ export default function CredentialsProvider({
|
||||
api,
|
||||
isLoggedIn,
|
||||
providerNames,
|
||||
systemProviders,
|
||||
createAPIKeyCredentials,
|
||||
createUserPasswordCredentials,
|
||||
createHostScopedCredentials,
|
||||
@@ -289,12 +280,12 @@ export default function CredentialsProvider({
|
||||
onFailToast,
|
||||
]);
|
||||
|
||||
// Fetch provider names and system providers on mount
|
||||
// Fetch provider names on mount
|
||||
useEffect(() => {
|
||||
Promise.all([api.listProviders(), api.listSystemProviders()])
|
||||
.then(([names, systemList]) => {
|
||||
api
|
||||
.listProviders()
|
||||
.then((names) => {
|
||||
setProviderNames(names);
|
||||
setSystemProviders(new Set(systemList));
|
||||
})
|
||||
.catch(onFailToast("Load provider names"));
|
||||
}, [api, onFailToast]);
|
||||
|
||||
@@ -22,7 +22,13 @@ const config = {
|
||||
poppins: ["var(--font-poppins)"],
|
||||
},
|
||||
colors: {
|
||||
// *** APPROVED DESIGN SYSTEM COLORS ***
|
||||
// These are the ONLY colors that should be used in our app
|
||||
...colors,
|
||||
|
||||
// Legacy colors - DO NOT USE THESE IN NEW CODE
|
||||
// These are kept only to prevent breaking existing styles
|
||||
// Use the approved design system colors above instead
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
@@ -57,66 +63,70 @@ const config = {
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
customGray: {
|
||||
"100": "#d9d9d9",
|
||||
"200": "#a8a8a8",
|
||||
"300": "#878787",
|
||||
"400": "#646464",
|
||||
"500": "#474747",
|
||||
"600": "#282828",
|
||||
"700": "#272727",
|
||||
100: "#d9d9d9",
|
||||
200: "#a8a8a8",
|
||||
300: "#878787",
|
||||
400: "#646464",
|
||||
500: "#474747",
|
||||
600: "#282828",
|
||||
700: "#272727",
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
"0": "0rem",
|
||||
"1": "0.25rem",
|
||||
"2": "0.5rem",
|
||||
"3": "0.75rem",
|
||||
"4": "1rem",
|
||||
"5": "1.25rem",
|
||||
"6": "1.5rem",
|
||||
"7": "1.75rem",
|
||||
"8": "2rem",
|
||||
"9": "2.25rem",
|
||||
"10": "2.5rem",
|
||||
"11": "2.75rem",
|
||||
"12": "3rem",
|
||||
"14": "3.5rem",
|
||||
"16": "4rem",
|
||||
"18": "4.5rem",
|
||||
"20": "5rem",
|
||||
"24": "6rem",
|
||||
"28": "7rem",
|
||||
"32": "8rem",
|
||||
"36": "9rem",
|
||||
"40": "10rem",
|
||||
"44": "11rem",
|
||||
"48": "12rem",
|
||||
"52": "13rem",
|
||||
"56": "14rem",
|
||||
"60": "15rem",
|
||||
"64": "16rem",
|
||||
"68": "17rem",
|
||||
"70": "17.5rem",
|
||||
"71": "17.75rem",
|
||||
"72": "18rem",
|
||||
"76": "19rem",
|
||||
"80": "20rem",
|
||||
"96": "24rem",
|
||||
"0.5": "0.125rem",
|
||||
"1.5": "0.375rem",
|
||||
"2.5": "0.625rem",
|
||||
"3.5": "0.875rem",
|
||||
"7.5": "1.875rem",
|
||||
"8.5": "2.125rem",
|
||||
// Tailwind spacing + custom sizes
|
||||
0: "0rem", // 0px
|
||||
0.5: "0.125rem", // 2px
|
||||
1: "0.25rem", // 4px
|
||||
1.5: "0.375rem", // 6px
|
||||
2: "0.5rem", // 8px
|
||||
2.5: "0.625rem", // 10px
|
||||
3: "0.75rem", // 12px
|
||||
3.5: "0.875rem", // 14px
|
||||
4: "1rem", // 16px
|
||||
5: "1.25rem", // 20px
|
||||
6: "1.5rem", // 24px
|
||||
7: "1.75rem", // 28px
|
||||
7.5: "1.875rem", // 30px
|
||||
8: "2rem", // 32px
|
||||
8.5: "2.125rem", // 34px
|
||||
9: "2.25rem", // 36px
|
||||
10: "2.5rem", // 40px
|
||||
11: "2.75rem", // 44px
|
||||
12: "3rem", // 48px
|
||||
14: "3.5rem", // 56px
|
||||
16: "4rem", // 64px
|
||||
18: "4.5rem", // 72px
|
||||
20: "5rem", // 80px
|
||||
24: "6rem", // 96px
|
||||
28: "7rem", // 112px
|
||||
32: "8rem", // 128px
|
||||
36: "9rem", // 144px
|
||||
40: "10rem", // 160px
|
||||
44: "11rem", // 176px
|
||||
48: "12rem", // 192px
|
||||
52: "13rem", // 208px
|
||||
56: "14rem", // 224px
|
||||
60: "15rem", // 240px
|
||||
64: "16rem", // 256px
|
||||
68: "17rem", // 272px
|
||||
70: "17.5rem", // 280px
|
||||
71: "17.75rem", // 284px
|
||||
72: "18rem", // 288px
|
||||
76: "19rem", // 304px
|
||||
80: "20rem", // 320px
|
||||
96: "24rem", // 384px
|
||||
},
|
||||
borderRadius: {
|
||||
xsmall: "0.25rem",
|
||||
small: "0.5rem",
|
||||
medium: "0.75rem",
|
||||
large: "1rem",
|
||||
xlarge: "1.25rem",
|
||||
"2xlarge": "1.5rem",
|
||||
full: "9999px",
|
||||
// Design system border radius tokens from Figma
|
||||
xsmall: "0.25rem", // 4px
|
||||
small: "0.5rem", // 8px
|
||||
medium: "0.75rem", // 12px
|
||||
large: "1rem", // 16px
|
||||
xlarge: "1.25rem", // 20px
|
||||
"2xlarge": "1.5rem", // 24px
|
||||
full: "9999px", // For pill buttons
|
||||
|
||||
// Legacy values - kept for backward compatibility
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
@@ -126,28 +136,16 @@ const config = {
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: {
|
||||
height: "0",
|
||||
},
|
||||
to: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
},
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
},
|
||||
to: {
|
||||
height: "0",
|
||||
},
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"fade-in": {
|
||||
"0%": {
|
||||
opacity: "0",
|
||||
},
|
||||
"100%": {
|
||||
opacity: "1",
|
||||
},
|
||||
"0%": { opacity: "0" }, // Start with opacity 0
|
||||
"100%": { opacity: "1" }, // End with opacity 1
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
|
||||
Reference in New Issue
Block a user