mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
working on credentials flow
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
};
|
||||
@@ -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'll need to add {providerName} credentials to continue
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user