diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx index 658a832646..a36d7e02ba 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx @@ -12,6 +12,7 @@ import { import { MessageSquareIcon } from "lucide-react"; import { UIMessage, UIDataTypes, UITools, ToolUIPart } from "ai"; import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks"; +import { FindAgentsTool } from "../../tools/FindAgents/FindAgents"; interface ChatMessagesContainerProps { messages: UIMessage[]; @@ -61,6 +62,14 @@ export const ChatMessagesContainer = ({ part={part as ToolUIPart} /> ); + case "tool-find_agent": + case "tool-find_library_agent": + return ( + + ); default: return null; } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx index 004464562d..b0ae5e87d4 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx @@ -9,7 +9,6 @@ import { } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; import { - SparkleIcon, PlusIcon, SpinnerGapIcon, ChatCircleIcon, diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx new file mode 100644 index 0000000000..c7807b727d --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/FindAgents.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { ToolUIPart } from "ai"; +import { CaretDownIcon } from "@phosphor-icons/react"; +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; +import Link from "next/link"; +import { useState } from "react"; +import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; +import { + getAgentHref, + getAnimationText, + getFindAgentsOutput, + getSourceLabelFromToolType, + StateIcon, +} from "./helpers"; +import { cn } from "@/lib/utils"; + +export interface FindAgentsToolPart { + type: string; + toolCallId: string; + state: ToolUIPart["state"]; + input?: unknown; + output?: unknown; +} + +interface Props { + part: FindAgentsToolPart; +} + +export function FindAgentsTool({ part }: Props) { + const text = getAnimationText(part); + const output = getFindAgentsOutput(part); + const shouldReduceMotion = useReducedMotion(); + const [isExpanded, setIsExpanded] = useState(false); + + const query = + typeof part.input === "object" && part.input !== null + ? String((part.input as { query?: unknown }).query ?? "").trim() + : ""; + + const isAgentsFound = + part.state === "output-available" && output?.type === "agents_found"; + const hasAgents = + isAgentsFound && + output.agents.length > 0 && + (typeof output.count !== "number" || output.count > 0); + const totalCount = isAgentsFound ? output.count : 0; + const { label: sourceLabel, source } = getSourceLabelFromToolType(part.type); + const scopeText = + source === "library" + ? "in your library" + : source === "marketplace" + ? "in marketplace" + : ""; + + return ( +
+
+ + +
+ + {hasAgents && ( +
+ + + + {isExpanded && ( + +
+ {output.agents.map((agent) => { + const href = getAgentHref(agent); + const agentSource = + agent.source === "library" + ? "Library" + : agent.source === "marketplace" + ? "Marketplace" + : null; + return ( +
+
+
+
+

+ {agent.name} +

+ {agentSource && ( + + {agentSource} + + )} +
+

+ {agent.description} +

+
+ {href && ( + + Open + + )} +
+
+ ); + })} +
+
+ )} +
+
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx new file mode 100644 index 0000000000..2f057369af --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/FindAgents/helpers.tsx @@ -0,0 +1,169 @@ +import { ToolUIPart } from "ai"; +import { + CheckCircleIcon, + CircleNotchIcon, + XCircleIcon, +} from "@phosphor-icons/react"; + +export interface FindAgentInput { + query: string; +} + +export interface AgentInfo { + id: string; + name: string; + description: string; + source?: "marketplace" | "library" | string; +} + +export interface AgentsFoundOutput { + type: "agents_found"; + title?: string; + message?: string; + session_id?: string; + agents: AgentInfo[]; + count: number; +} + +export interface NoResultsOutput { + type: "no_results"; + message: string; + suggestions?: string[]; + session_id?: string; +} + +export interface ErrorOutput { + type: "error"; + message: string; + error?: string; + session_id?: string; +} + +export type FindAgentsOutput = + | AgentsFoundOutput + | NoResultsOutput + | ErrorOutput; + +export type FindAgentsToolType = + | "tool-find_agent" + | "tool-find_library_agent" + | (string & {}); + +function parseOutput(output: unknown): FindAgentsOutput | null { + if (!output) return null; + if (typeof output === "string") { + const trimmed = output.trim(); + if (!trimmed) return null; + try { + return JSON.parse(trimmed) as FindAgentsOutput; + } catch { + return null; + } + } + if (typeof output === "object") { + return output as FindAgentsOutput; + } + return null; +} + +export function getFindAgentsOutput(part: unknown): FindAgentsOutput | null { + if (!part || typeof part !== "object") return null; + return parseOutput((part as { output?: unknown }).output); +} + +export function getSourceLabelFromToolType(toolType?: FindAgentsToolType): { + source: "marketplace" | "library" | "unknown"; + label: string; +} { + if (toolType === "tool-find_library_agent") { + return { source: "library", label: "Library" }; + } + if (toolType === "tool-find_agent") { + return { source: "marketplace", label: "Marketplace" }; + } + return { source: "unknown", label: "Agents" }; +} + +export function getAnimationText(part: { + type?: FindAgentsToolType; + state: ToolUIPart["state"]; + input?: unknown; + output?: unknown; +}): string { + const { label, source } = getSourceLabelFromToolType(part.type); + switch (part.state) { + case "input-streaming": + return `Searching ${label.toLowerCase()} agents for you`; + + case "input-available": { + const query = (part.input as FindAgentInput | undefined)?.query?.trim(); + if (query) { + return source === "library" + ? `Finding library agents matching "${query}"` + : `Finding marketplace agents matching "${query}"`; + } + return source === "library" ? "Finding library agents" : "Finding agents"; + } + + case "output-available": { + const output = parseOutput(part.output); + const query = (part.input as FindAgentInput | undefined)?.query?.trim(); + const scope = source === "library" ? "in your library" : "in marketplace"; + if (!output) { + return query ? `Found agents ${scope} for "${query}"` : "Found agents"; + } + if (output.type === "no_results") { + return query + ? `No agents found ${scope} for "${query}"` + : `No agents found ${scope}`; + } + if (output.type === "agents_found") { + const count = output.count ?? output.agents?.length ?? 0; + const countText = `Found ${count} agent${count === 1 ? "" : "s"}`; + if (query) return `${countText} ${scope} for "${query}"`; + return `${countText} ${scope}`; + } + if (output.type === "error") { + return `Error finding agents ${scope}`; + } + return `Found agents ${scope}`; + } + + case "output-error": + return source === "library" + ? "Error finding agents in your library" + : "Error finding agents in marketplace"; + + default: + return "Processing"; + } +} + +export function getAgentHref(agent: AgentInfo): string | null { + if (agent.source === "library") { + return `/library/agents/${encodeURIComponent(agent.id)}`; + } + + const [creator, slug, ...rest] = agent.id.split("/"); + if (!creator || !slug || rest.length > 0) return null; + return `/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`; +} + +export function StateIcon({ state }: { state: ToolUIPart["state"] }) { + switch (state) { + case "input-streaming": + case "input-available": + return ( + + ); + case "output-available": + return ; + case "output-error": + return ; + default: + return null; + } +} diff --git a/autogpt_platform/frontend/src/components/ai-elements/message.tsx b/autogpt_platform/frontend/src/components/ai-elements/message.tsx index 071aed2de8..aa2dcfe831 100644 --- a/autogpt_platform/frontend/src/components/ai-elements/message.tsx +++ b/autogpt_platform/frontend/src/components/ai-elements/message.tsx @@ -211,7 +211,7 @@ export type MessageBranchSelectorProps = HTMLAttributes & { export const MessageBranchSelector = ({ className, - from, + from: _from, ...props }: MessageBranchSelectorProps) => { const { totalBranches } = useMessageBranch(); @@ -223,7 +223,10 @@ export const MessageBranchSelector = ({ return ( *:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md", + className, + )} orientation="horizontal" {...props} /> diff --git a/autogpt_platform/frontend/src/components/ui/button.tsx b/autogpt_platform/frontend/src/components/ui/button.tsx index 7084276cca..1cd93355cf 100644 --- a/autogpt_platform/frontend/src/components/ui/button.tsx +++ b/autogpt_platform/frontend/src/components/ui/button.tsx @@ -26,6 +26,7 @@ const buttonVariants = cva( sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9", + "icon-sm": "h-8 w-8", }, }, defaultVariants: {