diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx index a39cfddddd..8261549a80 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/RunAgent.tsx @@ -1,16 +1,10 @@ "use client"; import type { ToolUIPart } from "ai"; -import Link from "next/link"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; -import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; import { - ChatCredentialsSetup, - type CredentialInfo, -} from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup"; -import { - formatMaybeJson, + getAccordionMeta, getAnimationText, getRunAgentToolOutput, isRunAgentAgentDetailsOutput, @@ -19,8 +13,11 @@ import { isRunAgentNeedLoginOutput, isRunAgentSetupRequirementsOutput, ToolIcon, - type RunAgentToolOutput, } from "./helpers"; +import { ExecutionStartedCard } from "./components/ExecutionStartedCard/ExecutionStartedCard"; +import { AgentDetailsCard } from "./components/AgentDetailsCard/AgentDetailsCard"; +import { SetupRequirementsCard } from "./components/SetupRequirementsCard/SetupRequirementsCard"; +import { ErrorCard } from "./components/ErrorCard/ErrorCard"; export interface RunAgentToolPart { type: string; @@ -34,177 +31,8 @@ interface Props { part: RunAgentToolPart; } -function getAccordionMeta(output: RunAgentToolOutput): { - badgeText: string; - title: string; - description?: string; -} { - if (isRunAgentExecutionStartedOutput(output)) { - const statusText = - typeof output.status === "string" && output.status.trim() - ? output.status.trim() - : "started"; - return { - badgeText: "Run agent", - title: output.graph_name, - description: `Status: ${statusText}`, - }; - } - - if (isRunAgentAgentDetailsOutput(output)) { - return { - badgeText: "Run agent", - title: output.agent.name, - description: "Inputs required", - }; - } - - if (isRunAgentSetupRequirementsOutput(output)) { - const missingCredsCount = Object.keys( - (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record< - string, - unknown - >, - ).length; - return { - badgeText: "Run agent", - title: output.setup_info.agent_name, - description: - missingCredsCount > 0 - ? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}` - : output.message, - }; - } - - if (isRunAgentNeedLoginOutput(output)) { - return { badgeText: "Run agent", title: "Sign in required" }; - } - - return { badgeText: "Run agent", title: "Error" }; -} - -function coerceMissingCredentials( - rawMissingCredentials: unknown, -): CredentialInfo[] { - const missing = - rawMissingCredentials && typeof rawMissingCredentials === "object" - ? (rawMissingCredentials as Record) - : {}; - - const validTypes = new Set([ - "api_key", - "oauth2", - "user_password", - "host_scoped", - ]); - - const results: CredentialInfo[] = []; - - Object.values(missing).forEach((value) => { - if (!value || typeof value !== "object") return; - const cred = value as Record; - - const provider = - typeof cred.provider === "string" ? cred.provider.trim() : ""; - if (!provider) return; - - const providerName = - typeof cred.provider_name === "string" && cred.provider_name.trim() - ? cred.provider_name.trim() - : provider.replace(/_/g, " "); - - const title = - typeof cred.title === "string" && cred.title.trim() - ? cred.title.trim() - : providerName; - - const types = - Array.isArray(cred.types) && cred.types.length > 0 - ? cred.types - : typeof cred.type === "string" - ? [cred.type] - : []; - - const credentialTypes = types - .map((t) => (typeof t === "string" ? t.trim() : "")) - .filter( - (t): t is "api_key" | "oauth2" | "user_password" | "host_scoped" => - validTypes.has(t), - ); - - if (credentialTypes.length === 0) return; - - const scopes = Array.isArray(cred.scopes) - ? cred.scopes.filter((s): s is string => typeof s === "string") - : undefined; - - const item: CredentialInfo = { - provider, - providerName, - credentialTypes, - title, - }; - if (scopes && scopes.length > 0) { - item.scopes = scopes; - } - results.push(item); - }); - - return results; -} - -function coerceExpectedInputs(rawInputs: unknown): Array<{ - name: string; - title: string; - type: string; - description?: string; - required: boolean; -}> { - if (!Array.isArray(rawInputs)) return []; - const results: Array<{ - name: string; - title: string; - type: string; - description?: string; - required: boolean; - }> = []; - - rawInputs.forEach((value, index) => { - if (!value || typeof value !== "object") return; - const input = value as Record; - - const name = - typeof input.name === "string" && input.name.trim() - ? input.name.trim() - : `input-${index}`; - const title = - typeof input.title === "string" && input.title.trim() - ? input.title.trim() - : name; - const type = typeof input.type === "string" ? input.type : "unknown"; - const description = - typeof input.description === "string" && input.description.trim() - ? input.description.trim() - : undefined; - const required = Boolean(input.required); - - const item: { - name: string; - title: string; - type: string; - description?: string; - required: boolean; - } = { name, title, type, required }; - if (description) item.description = description; - results.push(item); - }); - - return results; -} - export function RunAgentTool({ part }: Props) { const text = getAnimationText(part); - const { onSend } = useCopilotChatActions(); const isStreaming = part.state === "input-streaming" || part.state === "input-available"; @@ -221,12 +49,6 @@ export function RunAgentTool({ part }: Props) { isRunAgentNeedLoginOutput(output) || isRunAgentErrorOutput(output)); - function handleAllCredentialsComplete() { - onSend( - "I've configured the required credentials. Please check if everything is ready and proceed with running the agent.", - ); - } - return (
@@ -246,130 +68,22 @@ export function RunAgentTool({ part }: Props) { } > {isRunAgentExecutionStartedOutput(output) && ( -
-
-
-
-

- Execution started -

-

- {output.execution_id} -

-

- {output.message} -

-
- {output.library_agent_link && ( - - Open - - )} -
-
-
+ )} {isRunAgentAgentDetailsOutput(output) && ( -
-

{output.message}

- - {output.agent.description?.trim() && ( -

- {output.agent.description} -

- )} - -
-

Inputs

-

- Provide required inputs in chat, or ask to run with defaults. -

-
-                  {formatMaybeJson(output.agent.inputs)}
-                
-
-
+ )} {isRunAgentSetupRequirementsOutput(output) && ( -
-

{output.message}

- - {coerceMissingCredentials( - output.setup_info.user_readiness?.missing_credentials, - ).length > 0 && ( - {}} - /> - )} - - {coerceExpectedInputs( - (output.setup_info.requirements as Record) - ?.inputs, - ).length > 0 && ( -
-

- Expected inputs -

-
- {coerceExpectedInputs( - ( - output.setup_info.requirements as Record< - string, - unknown - > - )?.inputs, - ).map((input) => ( -
-
-

- {input.title} -

- - {input.required ? "Required" : "Optional"} - -
-

- {input.name} • {input.type} - {input.description ? ` • ${input.description}` : ""} -

-
- ))} -
-
- )} -
+ )} {isRunAgentNeedLoginOutput(output) && (

{output.message}

)} - {isRunAgentErrorOutput(output) && ( -
-

{output.message}

- {output.error && ( -
-                  {formatMaybeJson(output.error)}
-                
- )} - {output.details && ( -
-                  {formatMaybeJson(output.details)}
-                
- )} -
- )} + {isRunAgentErrorOutput(output) && } )}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/AgentDetailsCard/AgentDetailsCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/AgentDetailsCard/AgentDetailsCard.tsx new file mode 100644 index 0000000000..22bb763ceb --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/AgentDetailsCard/AgentDetailsCard.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Button } from "@/components/atoms/Button/Button"; +import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer"; +import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentDetailsResponse"; +import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions"; +import { buildInputSchema } from "./helpers"; + +interface Props { + output: AgentDetailsResponse; +} + +export function AgentDetailsCard({ output }: Props) { + const { onSend } = useCopilotChatActions(); + const [showInputForm, setShowInputForm] = useState(false); + const [inputValues, setInputValues] = useState>({}); + + function handleRunWithExamples() { + onSend( + `Run the agent "${output.agent.name}" with placeholder/example values so I can test it.`, + ); + } + + function handleRunWithInputs() { + const nonEmpty = Object.fromEntries( + Object.entries(inputValues).filter( + ([, v]) => v !== undefined && v !== null && v !== "", + ), + ); + onSend( + `Run the agent "${output.agent.name}" with these inputs: ${JSON.stringify(nonEmpty, null, 2)}`, + ); + setShowInputForm(false); + setInputValues({}); + } + + return ( +
+

+ Run this agent with example values or your own inputs. +

+ +
+ + +
+ + + {showInputForm && buildInputSchema(output.agent.inputs) && ( + +
+

+ Enter your inputs +

+ setInputValues(v.formData ?? {})} + uiSchema={{ + "ui:submitButtonOptions": { norender: true }, + }} + initialValues={inputValues} + formContext={{ + showHandles: false, + size: "small", + }} + /> +
+ + +
+
+
+ )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/AgentDetailsCard/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/AgentDetailsCard/helpers.ts new file mode 100644 index 0000000000..635b8d20d7 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/AgentDetailsCard/helpers.ts @@ -0,0 +1,8 @@ +import type { RJSFSchema } from "@rjsf/utils"; + +export function buildInputSchema(inputs: unknown): RJSFSchema | null { + if (!inputs || typeof inputs !== "object") return null; + const properties = inputs as RJSFSchema["properties"]; + if (!properties || Object.keys(properties).length === 0) return null; + return inputs as RJSFSchema; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/ErrorCard/ErrorCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/ErrorCard/ErrorCard.tsx new file mode 100644 index 0000000000..9f83ca383d --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/ErrorCard/ErrorCard.tsx @@ -0,0 +1,26 @@ +"use client"; + +import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse"; +import { formatMaybeJson } from "../../helpers"; + +interface Props { + output: ErrorResponse; +} + +export function ErrorCard({ output }: Props) { + return ( +
+

{output.message}

+ {output.error && ( +
+          {formatMaybeJson(output.error)}
+        
+ )} + {output.details && ( +
+          {formatMaybeJson(output.details)}
+        
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/ExecutionStartedCard/ExecutionStartedCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/ExecutionStartedCard/ExecutionStartedCard.tsx new file mode 100644 index 0000000000..2e2b48fae6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/ExecutionStartedCard/ExecutionStartedCard.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button } from "@/components/atoms/Button/Button"; +import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse"; + +interface Props { + output: ExecutionStartedResponse; +} + +export function ExecutionStartedCard({ output }: Props) { + const router = useRouter(); + + return ( +
+
+
+

+ Execution started +

+

+ {output.execution_id} +

+

{output.message}

+
+ {output.library_agent_link && ( + + )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/SetupRequirementsCard/SetupRequirementsCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/SetupRequirementsCard/SetupRequirementsCard.tsx new file mode 100644 index 0000000000..77b8360681 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/SetupRequirementsCard/SetupRequirementsCard.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useState } from "react"; +import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView"; +import { Button } from "@/components/atoms/Button/Button"; +import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types"; +import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse"; +import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions"; +import { coerceCredentialFields, coerceExpectedInputs } from "./helpers"; + +interface Props { + output: SetupRequirementsResponse; +} + +export function SetupRequirementsCard({ output }: Props) { + const { onSend } = useCopilotChatActions(); + + const [inputCredentials, setInputCredentials] = useState< + Record + >({}); + const [hasSent, setHasSent] = useState(false); + + const { credentialFields, requiredCredentials } = coerceCredentialFields( + output.setup_info.user_readiness?.missing_credentials, + ); + + const expectedInputs = coerceExpectedInputs( + (output.setup_info.requirements as Record)?.inputs, + ); + + function handleCredentialChange(key: string, value?: CredentialsMetaInput) { + setInputCredentials((prev) => ({ ...prev, [key]: value })); + } + + const isAllComplete = + credentialFields.length > 0 && + [...requiredCredentials].every((key) => !!inputCredentials[key]); + + function handleProceed() { + setHasSent(true); + onSend( + "I've configured the required credentials. Please check if everything is ready and proceed with running the agent.", + ); + } + + return ( +
+

{output.message}

+ + {credentialFields.length > 0 && ( +
+ + {isAllComplete && !hasSent && ( + + )} +
+ )} + + {expectedInputs.length > 0 && ( +
+

Expected inputs

+
+ {expectedInputs.map((input) => ( +
+
+

+ {input.title} +

+ + {input.required ? "Required" : "Optional"} + +
+

+ {input.name} • {input.type} + {input.description ? ` \u2022 ${input.description}` : ""} +

+
+ ))} +
+
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/SetupRequirementsCard/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/SetupRequirementsCard/helpers.ts new file mode 100644 index 0000000000..6bb10751f0 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/components/SetupRequirementsCard/helpers.ts @@ -0,0 +1,116 @@ +import type { CredentialField } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers"; + +const VALID_CREDENTIAL_TYPES = new Set([ + "api_key", + "oauth2", + "user_password", + "host_scoped", +]); + +/** + * Transforms raw missing_credentials from SetupRequirementsResponse + * into CredentialField[] tuples compatible with CredentialsGroupedView. + * + * Each CredentialField is [key, schema] where schema matches + * BlockIOCredentialsSubSchema shape. + */ +export function coerceCredentialFields(rawMissingCredentials: unknown): { + credentialFields: CredentialField[]; + requiredCredentials: Set; +} { + const missing = + rawMissingCredentials && typeof rawMissingCredentials === "object" + ? (rawMissingCredentials as Record) + : {}; + + const credentialFields: CredentialField[] = []; + const requiredCredentials = new Set(); + + Object.entries(missing).forEach(([key, value]) => { + if (!value || typeof value !== "object") return; + const cred = value as Record; + + const provider = + typeof cred.provider === "string" ? cred.provider.trim() : ""; + if (!provider) return; + + const types = + Array.isArray(cred.types) && cred.types.length > 0 + ? cred.types + : typeof cred.type === "string" + ? [cred.type] + : []; + + const credentialTypes = types + .map((t) => (typeof t === "string" ? t.trim() : "")) + .filter((t) => VALID_CREDENTIAL_TYPES.has(t)); + + if (credentialTypes.length === 0) return; + + const scopes = Array.isArray(cred.scopes) + ? cred.scopes.filter((s): s is string => typeof s === "string") + : undefined; + + const schema = { + type: "object" as const, + properties: {}, + credentials_provider: [provider], + credentials_types: credentialTypes, + credentials_scopes: scopes, + }; + + credentialFields.push([key, schema]); + requiredCredentials.add(key); + }); + + return { credentialFields, requiredCredentials }; +} + +export function coerceExpectedInputs(rawInputs: unknown): Array<{ + name: string; + title: string; + type: string; + description?: string; + required: boolean; +}> { + if (!Array.isArray(rawInputs)) return []; + const results: Array<{ + name: string; + title: string; + type: string; + description?: string; + required: boolean; + }> = []; + + rawInputs.forEach((value, index) => { + if (!value || typeof value !== "object") return; + const input = value as Record; + + const name = + typeof input.name === "string" && input.name.trim() + ? input.name.trim() + : `input-${index}`; + const title = + typeof input.title === "string" && input.title.trim() + ? input.title.trim() + : name; + const type = typeof input.type === "string" ? input.type : "unknown"; + const description = + typeof input.description === "string" && input.description.trim() + ? input.description.trim() + : undefined; + const required = Boolean(input.required); + + const item: { + name: string; + title: string; + type: string; + description?: string; + required: boolean; + } = { name, title, type, required }; + if (description) item.description = description; + results.push(item); + }); + + return results; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx index a176456a48..f969298d68 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/RunAgent/helpers.tsx @@ -189,3 +189,53 @@ export function formatMaybeJson(value: unknown): string { return String(value); } } + + +export function getAccordionMeta(output: RunAgentToolOutput): { + badgeText: string; + title: string; + description?: string; +} { + if (isRunAgentExecutionStartedOutput(output)) { + const statusText = + typeof output.status === "string" && output.status.trim() + ? output.status.trim() + : "started"; + return { + badgeText: "Run agent", + title: output.graph_name, + description: `Status: ${statusText}`, + }; + } + + if (isRunAgentAgentDetailsOutput(output)) { + return { + badgeText: "Run agent", + title: output.agent.name, + description: "Inputs required", + }; + } + + if (isRunAgentSetupRequirementsOutput(output)) { + const missingCredsCount = Object.keys( + (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record< + string, + unknown + >, + ).length; + return { + badgeText: "Run agent", + title: output.setup_info.agent_name, + description: + missingCredsCount > 0 + ? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}` + : output.message, + }; + } + + if (isRunAgentNeedLoginOutput(output)) { + return { badgeText: "Run agent", title: "Sign in required" }; + } + + return { badgeText: "Run agent", title: "Error" }; +} \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx index ade60c5ca9..0b82914abe 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/ViewAgentOutput/ViewAgentOutput.tsx @@ -2,6 +2,8 @@ import type { ToolUIPart } from "ai"; import Link from "next/link"; +import React, { useState } from "react"; +import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; import { @@ -46,6 +48,87 @@ function getAccordionMeta(output: ViewAgentOutputToolOutput): { return { badgeText: "Agent output", title: "Error" }; } +function resolveWorkspaceUrl(src: string): string { + if (src.startsWith("workspace://")) { + const withoutPrefix = src.replace("workspace://", ""); + const fileId = withoutPrefix.split("#")[0]; + const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId); + return `/api/proxy${apiPath}`; + } + return src; +} + +function getWorkspaceMimeHint(src: string): string | undefined { + const hashIndex = src.indexOf("#"); + if (hashIndex === -1) return undefined; + return src.slice(hashIndex + 1) || undefined; +} + +function WorkspaceMedia({ value }: { value: string }) { + const [imgFailed, setImgFailed] = useState(false); + const resolvedUrl = resolveWorkspaceUrl(value); + const mime = getWorkspaceMimeHint(value); + + if (mime?.startsWith("video/") || imgFailed) { + return ( + + ); + } + + if (mime?.startsWith("audio/")) { + return