add find agent tool in coiplot-2

This commit is contained in:
abhi1992002
2026-02-03 13:04:04 +05:30
parent d2a1abe3f8
commit 8c381faa06
6 changed files with 342 additions and 3 deletions

View File

@@ -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<unknown, UIDataTypes, UITools>[];
@@ -61,6 +62,14 @@ export const ChatMessagesContainer = ({
part={part as ToolUIPart}
/>
);
case "tool-find_agent":
case "tool-find_library_agent":
return (
<FindAgentsTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
default:
return null;
}

View File

@@ -9,7 +9,6 @@ import {
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import {
SparkleIcon,
PlusIcon,
SpinnerGapIcon,
ChatCircleIcon,

View File

@@ -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 (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
{hasAgents && (
<div className="mt-2 rounded-2xl border bg-background px-3 py-2">
<button
type="button"
aria-expanded={isExpanded}
onClick={() => setIsExpanded((v) => !v)}
className="flex w-full items-center justify-between gap-3 py-1 text-left"
>
<div className="flex min-w-0 items-center gap-2">
<span className="rounded-full border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
{sourceLabel}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">
Agent results
</p>
<p className="truncate text-xs text-muted-foreground">
Found {totalCount} {scopeText}
{query ? ` for "${query}"` : ""}
</p>
</div>
</div>
<CaretDownIcon
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
isExpanded && "rotate-180",
)}
weight="bold"
/>
</button>
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0, filter: "blur(10px)" }}
animate={{ height: "auto", opacity: 1, filter: "blur(0px)" }}
exit={{ height: 0, opacity: 0, filter: "blur(10px)" }}
transition={
shouldReduceMotion
? { duration: 0 }
: { type: "spring", bounce: 0.35, duration: 0.55 }
}
className="overflow-hidden"
style={{ willChange: "height, opacity, filter" }}
>
<div className="grid gap-2 pb-2 pt-3 sm:grid-cols-2">
{output.agents.map((agent) => {
const href = getAgentHref(agent);
const agentSource =
agent.source === "library"
? "Library"
: agent.source === "marketplace"
? "Marketplace"
: null;
return (
<div
key={agent.id}
className="rounded-2xl border bg-background p-3"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="truncate text-sm font-medium text-foreground">
{agent.name}
</p>
{agentSource && (
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
{agentSource}
</span>
)}
</div>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{agent.description}
</p>
</div>
{href && (
<Link
href={href}
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open
</Link>
)}
</div>
</div>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
);
}

View File

@@ -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 (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case "output-error":
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return null;
}
}

View File

@@ -211,7 +211,7 @@ export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
export const MessageBranchSelector = ({
className,
from,
from: _from,
...props
}: MessageBranchSelectorProps) => {
const { totalBranches } = useMessageBranch();
@@ -223,7 +223,10 @@ export const MessageBranchSelector = ({
return (
<ButtonGroup
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
className={cn(
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
className,
)}
orientation="horizontal"
{...props}
/>

View File

@@ -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: {