working on credentials flow

This commit is contained in:
Swifty
2025-11-04 10:54:39 +01:00
parent 3e68615b33
commit 93551a905c
4 changed files with 110 additions and 452 deletions

View File

@@ -1,14 +1,11 @@
import { useEffect } from "react";
import { Card } from "@/components/atoms/Card/Card";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Key, Check, Warning } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { useChatCredentialsSetup } from "./useChatCredentialsSetup";
import { APIKeyCredentialsModal } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/APIKeyCredentialsModal/APIKeyCredentialsModal";
import { OAuthFlowWaitingModal } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/OAuthWaitingModal/OAuthWaitingModal";
import { PasswordCredentialsModal } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/PasswordCredentialsModal/PasswordCredentialsModal";
import { HostScopedCredentialsModal } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/HotScopedCredentialsModal/HotScopedCredentialsModal";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
import type { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
export interface CredentialInfo {
provider: string;
@@ -27,6 +24,21 @@ interface Props {
className?: string;
}
function createSchemaFromCredentialInfo(
credential: CredentialInfo
): BlockIOCredentialsSubSchema {
return {
type: "object",
properties: {},
credentials_provider: [credential.provider],
credentials_types: [credential.credentialType],
credentials_scopes: credential.scopes,
discriminator: undefined,
discriminator_mapping: undefined,
discriminator_values: undefined,
};
}
export function ChatCredentialsSetup({
credentials,
agentName,
@@ -35,14 +47,8 @@ export function ChatCredentialsSetup({
onCancel,
className,
}: Props) {
const {
credentialStatuses,
isAllComplete,
activeModal,
handleSetupClick,
handleModalClose,
handleCredentialCreated,
} = useChatCredentialsSetup(credentials);
const { selectedCredentials, isAllComplete, handleCredentialSelect } =
useChatCredentialsSetup(credentials);
// Auto-call completion when all credentials are configured
useEffect(
@@ -55,140 +61,82 @@ export function ChatCredentialsSetup({
);
return (
<>
<Card
className={cn(
"mx-4 my-2 overflow-hidden border-orange-200 bg-orange-50 dark:border-orange-900 dark:bg-orange-950",
className
)}
>
<div className="flex items-start gap-4 p-6">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-500">
<Key size={24} weight="bold" className="text-white" />
</div>
<div className="flex-1">
<Text
variant="h3"
className="mb-2 text-orange-900 dark:text-orange-100"
>
Credentials Required
</Text>
<Text
variant="body"
className="mb-4 text-orange-700 dark:text-orange-300"
>
{message}
</Text>
<Card
className={cn(
"mx-4 my-2 overflow-hidden border-orange-200 bg-orange-50 dark:border-orange-900 dark:bg-orange-950",
className
)}
>
<div className="flex items-start gap-4 p-6">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-500">
<Key size={24} weight="bold" className="text-white" />
</div>
<div className="flex-1">
<Text
variant="h3"
className="mb-2 text-orange-900 dark:text-orange-100"
>
Credentials Required
</Text>
<Text
variant="body"
className="mb-4 text-orange-700 dark:text-orange-300"
>
{message}
</Text>
<div className="space-y-3">
{credentials.map((cred, index) => (
<CredentialRow
<div className="space-y-3">
{credentials.map((cred, index) => {
const schema = createSchemaFromCredentialInfo(cred);
const isSelected = !!selectedCredentials[cred.provider];
return (
<div
key={`${cred.provider}-${index}`}
credential={cred}
status={credentialStatuses[index]}
onSetup={() => handleSetupClick(index, cred)}
/>
))}
</div>
className={cn(
"relative rounded-lg border border-orange-200 bg-white p-4 dark:border-orange-800 dark:bg-orange-900/20",
isSelected &&
"border-green-500 bg-green-50 dark:border-green-700 dark:bg-green-950/30"
)}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
{isSelected ? (
<Check
size={20}
className="text-green-500"
weight="bold"
/>
) : (
<Warning
size={20}
className="text-orange-500"
weight="bold"
/>
)}
<Text
variant="body"
className="font-semibold text-orange-900 dark:text-orange-100"
>
{cred.providerName}
</Text>
</div>
</div>
<CredentialsInput
schema={schema}
selectedCredentials={selectedCredentials[cred.provider]}
onSelectCredentials={(credMeta) =>
handleCredentialSelect(cred.provider, credMeta)
}
hideIfSingleCredentialAvailable={false}
/>
</div>
);
})}
</div>
</div>
<div className="border-t border-orange-200 px-6 py-4 dark:border-orange-900">
<Button variant="secondary" onClick={onCancel}>
Cancel
</Button>
</div>
</Card>
{/* Modals - reuse existing components */}
{activeModal?.type === "api_key" && activeModal.schema && (
<APIKeyCredentialsModal
schema={activeModal.schema}
open={true}
onClose={handleModalClose}
onCredentialsCreate={handleCredentialCreated}
/>
)}
{activeModal?.type === "oauth2" && (
<OAuthFlowWaitingModal
open={true}
onClose={handleModalClose}
providerName={activeModal.providerName || ""}
/>
)}
{activeModal?.type === "user_password" && activeModal.schema && (
<PasswordCredentialsModal
schema={activeModal.schema}
open={true}
onClose={handleModalClose}
onCredentialsCreate={handleCredentialCreated}
/>
)}
{activeModal?.type === "host_scoped" && activeModal.schema && (
<HostScopedCredentialsModal
schema={activeModal.schema}
open={true}
onClose={handleModalClose}
onCredentialsCreate={handleCredentialCreated}
/>
)}
</>
);
}
interface CredentialRowProps {
credential: CredentialInfo;
status?: {
isConfigured: boolean;
credentialId?: string;
};
onSetup: () => void;
}
function CredentialRow({ credential, status, onSetup }: CredentialRowProps) {
const isConfigured = status?.isConfigured || false;
function getCredentialTypeLabel(type: string): string {
switch (type) {
case "api_key":
return "API Key";
case "oauth2":
return "OAuth";
case "user_password":
return "Username & Password";
case "host_scoped":
return "Custom Headers";
default:
return "Credentials";
}
}
return (
<div className="flex items-center justify-between rounded-lg border border-orange-200 bg-white p-3 dark:border-orange-800 dark:bg-orange-900/20">
<div className="flex items-center gap-3">
{isConfigured ? (
<Check size={20} className="text-green-500" weight="bold" />
) : (
<Warning size={20} className="text-orange-500" weight="bold" />
)}
<div>
<Text variant="body" className="font-semibold text-orange-900 dark:text-orange-100">
{credential.providerName}
</Text>
<Text variant="small" className="text-orange-700 dark:text-orange-300">
{getCredentialTypeLabel(credential.credentialType)}
</Text>
</div>
</div>
{!isConfigured && (
<Button size="small" onClick={onSetup} variant="primary">
Setup
</Button>
)}
</div>
</Card>
);
}

View File

@@ -1,116 +1,36 @@
import { useState, useEffect, useMemo, useContext } from "react";
import { toast } from "sonner";
import { useState, useEffect, useMemo } from "react";
import type { CredentialInfo } from "./ChatCredentialsSetup";
import type { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api";
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
interface CredentialStatus {
isConfigured: boolean;
credentialId?: string;
}
interface ActiveModal {
index: number;
type: "api_key" | "oauth2" | "user_password" | "host_scoped";
provider: string;
providerName: string;
schema?: BlockIOCredentialsSubSchema;
}
export function useChatCredentialsSetup(credentials: CredentialInfo[]) {
const allProviders = useContext(CredentialsProvidersContext);
const [credentialStatuses, setCredentialStatuses] = useState<
CredentialStatus[]
>([]);
const [activeModal, setActiveModal] = useState<ActiveModal | null>(null);
// Check existing credentials on mount
useEffect(
function checkExistingCredentials() {
const statuses = credentials.map((cred) => {
const provider = allProviders?.[cred.provider];
const hasSavedCredentials =
provider && provider.savedCredentials && provider.savedCredentials.length > 0;
return {
isConfigured: hasSavedCredentials || false,
credentialId: hasSavedCredentials ? provider.savedCredentials[0].id : undefined,
};
});
setCredentialStatuses(statuses);
},
[credentials, allProviders]
);
const [selectedCredentials, setSelectedCredentials] = useState<
Record<string, CredentialsMetaInput>
>({});
// Check if all credentials are configured
const isAllComplete = useMemo(
function checkAllComplete() {
if (credentialStatuses.length === 0) return false;
return credentialStatuses.every((status) => status.isConfigured);
if (credentials.length === 0) return false;
return credentials.every((cred) => selectedCredentials[cred.provider]);
},
[credentialStatuses]
[credentials, selectedCredentials]
);
function handleSetupClick(index: number, credential: CredentialInfo) {
const provider = allProviders?.[credential.provider];
if (!provider) {
toast.error("Provider not found", {
description: `Unable to find configuration for ${credential.providerName}`,
});
return;
}
// Create a minimal schema for the modal
const schema: BlockIOCredentialsSubSchema = {
type: "object",
properties: {},
credentials_provider: [credential.provider],
credentials_types: [credential.credentialType],
credentials_scopes: credential.scopes,
discriminator: undefined,
discriminator_mapping: undefined,
discriminator_values: undefined,
};
setActiveModal({
index,
type: credential.credentialType,
provider: credential.provider,
providerName: credential.providerName,
schema,
});
}
function handleModalClose() {
setActiveModal(null);
}
function handleCredentialCreated(credentialMeta: CredentialsMetaInput) {
if (activeModal) {
// Mark credential as complete
setCredentialStatuses((prev) => {
const updated = [...prev];
updated[activeModal.index] = {
isConfigured: true,
credentialId: credentialMeta.id,
};
return updated;
});
toast.success("Credential added successfully", {
description: `${activeModal.providerName} credentials have been configured`,
});
setActiveModal(null);
function handleCredentialSelect(
provider: string,
credential?: CredentialsMetaInput
) {
if (credential) {
setSelectedCredentials((prev) => ({
...prev,
[provider]: credential,
}));
}
}
return {
credentialStatuses,
selectedCredentials,
isAllComplete,
activeModal,
handleSetupClick,
handleModalClose,
handleCredentialCreated,
handleCredentialSelect,
};
}

View File

@@ -1,71 +0,0 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { CredentialsNeededPrompt } from "./CredentialsNeededPrompt";
const meta = {
title: "Molecules/CredentialsNeededPrompt",
component: CredentialsNeededPrompt,
parameters: {
layout: "padded",
},
tags: ["autodocs"],
args: {
onSetupCredentials: () => console.log("Setup credentials clicked"),
onCancel: () => console.log("Cancel clicked"),
},
} satisfies Meta<typeof CredentialsNeededPrompt>;
export default meta;
type Story = StoryObj<typeof meta>;
export const ApiKey: Story = {
args: {
provider: "openai",
providerName: "OpenAI",
credentialType: "api_key",
title: "GPT Agent",
message: "To run GPT Agent, you need to add OpenAI credentials.",
},
};
export const OAuth: Story = {
args: {
provider: "github",
providerName: "GitHub",
credentialType: "oauth2",
title: "GitHub Integration Agent",
message:
"To run GitHub Integration Agent, you need to add GitHub credentials.",
},
};
export const UserPassword: Story = {
args: {
provider: "database",
providerName: "Database Server",
credentialType: "user_password",
title: "Database Query Agent",
message:
"To run Database Query Agent, you need to add Database Server credentials.",
},
};
export const HostScoped: Story = {
args: {
provider: "custom_api",
providerName: "Custom API",
credentialType: "host_scoped",
title: "Custom API Agent",
message: "To run Custom API Agent, you need to add Custom API credentials.",
},
};
export const LongMessage: Story = {
args: {
provider: "slack",
providerName: "Slack",
credentialType: "oauth2",
title: "Slack Notification Agent for Team Collaboration and Updates",
message:
"To run Slack Notification Agent for Team Collaboration and Updates, you need to add Slack credentials. This will allow the agent to send messages, create channels, and manage workspace settings on your behalf.",
},
};

View File

@@ -1,139 +0,0 @@
import React from "react";
import { Text } from "@/components/atoms/Text/Text";
import { Key, ArrowRight } from "@phosphor-icons/react";
import { Button } from "@/components/atoms/Button/Button";
import { cn } from "@/lib/utils";
export interface CredentialsNeededPromptProps {
provider: string;
providerName: string;
credentialType: string;
title: string;
message: string;
onSetupCredentials: () => void;
onCancel: () => void;
className?: string;
}
export function CredentialsNeededPrompt({
provider: _provider,
providerName,
credentialType,
title,
message,
onSetupCredentials,
onCancel,
className,
}: CredentialsNeededPromptProps) {
function getCredentialTypeLabel(type: string): string {
switch (type) {
case "api_key":
return "API Key";
case "oauth2":
return "OAuth Connection";
case "user_password":
return "Username & Password";
case "host_scoped":
return "Custom Headers";
default:
return "Credentials";
}
}
return (
<div
className={cn(
"mx-4 my-2 flex flex-col gap-4 rounded-lg border border-orange-200 bg-orange-50 p-6 dark:border-orange-900 dark:bg-orange-950",
className,
)}
>
{/* Icon & Header */}
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-500">
<Key size={24} weight="bold" className="text-white" />
</div>
<div className="flex-1">
<Text
variant="h3"
className="mb-1 text-orange-900 dark:text-orange-100"
>
Credentials Required
</Text>
<Text variant="body" className="text-orange-700 dark:text-orange-300">
{message}
</Text>
</div>
</div>
{/* Details */}
<div className="rounded-md bg-orange-100 p-4 dark:bg-orange-900">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Text
variant="small"
className="font-semibold text-orange-900 dark:text-orange-100"
>
Provider:
</Text>
<Text
variant="body"
className="text-orange-800 dark:text-orange-200"
>
{providerName}
</Text>
</div>
<div className="flex items-center justify-between">
<Text
variant="small"
className="font-semibold text-orange-900 dark:text-orange-100"
>
Type:
</Text>
<Text
variant="body"
className="text-orange-800 dark:text-orange-200"
>
{getCredentialTypeLabel(credentialType)}
</Text>
</div>
<div className="flex items-center justify-between">
<Text
variant="small"
className="font-semibold text-orange-900 dark:text-orange-100"
>
Needed for:
</Text>
<Text
variant="body"
className="text-orange-800 dark:text-orange-200"
>
{title}
</Text>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<Button
onClick={onSetupCredentials}
variant="primary"
className="flex flex-1 items-center justify-center gap-2"
>
Setup Credentials
<ArrowRight size={20} weight="bold" />
</Button>
<Button onClick={onCancel} variant="secondary">
Cancel
</Button>
</div>
<Text
variant="small"
className="text-center text-orange-600 dark:text-orange-400"
>
You&apos;ll need to add {providerName} credentials to continue
</Text>
</div>
);
}