mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Refactor FindAgents and SearchDocs tools to use ToolAccordion for improved UI/UX
- Replaced custom expandable sections with ToolAccordion component in both FindAgents and SearchDocs tools. - Simplified state management by removing unnecessary useState and useReducedMotion hooks. - Enhanced accessibility and readability of agent and document search results with clearer descriptions and structured layouts.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import { useId } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useToolAccordion } from "./useToolAccordion";
|
||||
|
||||
interface Props {
|
||||
badgeText: string;
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
defaultExpanded?: boolean;
|
||||
expanded?: boolean;
|
||||
onExpandedChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
export function ToolAccordion({
|
||||
badgeText,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
defaultExpanded,
|
||||
expanded,
|
||||
onExpandedChange,
|
||||
}: Props) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const contentId = useId();
|
||||
const { isExpanded, toggle } = useToolAccordion({
|
||||
expanded,
|
||||
defaultExpanded,
|
||||
onExpandedChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 rounded-2xl border bg-background px-3 py-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={contentId}
|
||||
onClick={toggle}
|
||||
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">
|
||||
{badgeText}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{title}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{description}
|
||||
</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
|
||||
id={contentId}
|
||||
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="pb-2 pt-3">{children}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface UseToolAccordionOptions {
|
||||
expanded?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
onExpandedChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
interface UseToolAccordionResult {
|
||||
isExpanded: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export function useToolAccordion({
|
||||
expanded,
|
||||
defaultExpanded = false,
|
||||
onExpandedChange,
|
||||
}: UseToolAccordionOptions): UseToolAccordionResult {
|
||||
const [uncontrolledExpanded, setUncontrolledExpanded] =
|
||||
useState(defaultExpanded);
|
||||
|
||||
const isControlled = typeof expanded === "boolean";
|
||||
const isExpanded = isControlled ? expanded : uncontrolledExpanded;
|
||||
|
||||
function toggle() {
|
||||
const next = !isExpanded;
|
||||
if (!isControlled) setUncontrolledExpanded(next);
|
||||
onExpandedChange?.(next);
|
||||
}
|
||||
|
||||
return { isExpanded, toggle };
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
"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,
|
||||
@@ -13,7 +10,7 @@ import {
|
||||
getSourceLabelFromToolType,
|
||||
StateIcon,
|
||||
} from "./helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
|
||||
export interface FindAgentsToolPart {
|
||||
type: string;
|
||||
@@ -30,8 +27,6 @@ interface Props {
|
||||
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
|
||||
@@ -52,6 +47,9 @@ export function FindAgentsTool({ part }: Props) {
|
||||
: source === "marketplace"
|
||||
? "in marketplace"
|
||||
: "";
|
||||
const accordionDescription = `Found ${totalCount}${scopeText ? ` ${scopeText}` : ""}${
|
||||
query ? ` for "${query}"` : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
@@ -61,97 +59,55 @@ export function FindAgentsTool({ part }: Props) {
|
||||
</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>
|
||||
<ToolAccordion
|
||||
badgeText={sourceLabel}
|
||||
title="Agent results"
|
||||
description={accordionDescription}
|
||||
>
|
||||
<div className="grid gap-2 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>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
import type { ToolUIPart } from "ai";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import {
|
||||
getDocsToolOutput,
|
||||
getDocsToolTitle,
|
||||
@@ -36,9 +34,6 @@ function truncate(text: string, maxChars: number): string {
|
||||
}
|
||||
|
||||
export function SearchDocsTool({ part }: Props) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const output = getDocsToolOutput(part);
|
||||
const text = getAnimationText(part);
|
||||
|
||||
@@ -58,6 +53,15 @@ export function SearchDocsTool({ part }: Props) {
|
||||
output.type === "no_results" ||
|
||||
output.type === "error");
|
||||
|
||||
const accordionDescription =
|
||||
hasExpandableContent && output
|
||||
? output.type === "doc_search_results"
|
||||
? `Found ${output.count} result${output.count === 1 ? "" : "s"} for "${output.query}"`
|
||||
: output.type === "doc_page"
|
||||
? output.path
|
||||
: output.message
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
@@ -66,103 +70,35 @@ export function SearchDocsTool({ part }: Props) {
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && normalized && (
|
||||
<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">
|
||||
{normalized.label}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{normalized.title}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{output.type === "doc_search_results"
|
||||
? `Found ${output.count} result${output.count === 1 ? "" : "s"} for "${output.query}"`
|
||||
: output.type === "doc_page"
|
||||
? output.path
|
||||
: output.message}
|
||||
</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" }}
|
||||
>
|
||||
{output.type === "doc_search_results" && (
|
||||
<div className="grid gap-2 pb-2 pt-3">
|
||||
{output.results.map((r) => {
|
||||
const href = r.doc_url ?? toDocsUrl(r.path);
|
||||
return (
|
||||
<div
|
||||
key={r.path}
|
||||
className="rounded-2xl border bg-background p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{r.title}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{r.path}
|
||||
{r.section ? ` • ${r.section}` : ""}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{truncate(r.snippet, 240)}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "doc_page" && (
|
||||
<div className="pb-2 pt-3">
|
||||
<ToolAccordion
|
||||
badgeText={normalized.label}
|
||||
title={normalized.title}
|
||||
description={accordionDescription}
|
||||
>
|
||||
{output.type === "doc_search_results" && (
|
||||
<div className="grid gap-2">
|
||||
{output.results.map((r) => {
|
||||
const href = r.doc_url ?? toDocsUrl(r.path);
|
||||
return (
|
||||
<div
|
||||
key={r.path}
|
||||
className="rounded-2xl border bg-background p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{output.title}
|
||||
{r.title}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{output.path}
|
||||
{r.path}
|
||||
{r.section ? ` • ${r.section}` : ""}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{truncate(r.snippet, 240)}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={output.doc_url ?? toDocsUrl(output.path)}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
@@ -170,39 +106,62 @@ export function SearchDocsTool({ part }: Props) {
|
||||
Open
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{truncate(output.content, 800)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "no_results" && (
|
||||
<div className="pb-2 pt-3">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.suggestions && output.suggestions.length > 0 && (
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
|
||||
{output.suggestions.slice(0, 5).map((s) => (
|
||||
<li key={s}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{output.type === "doc_page" && (
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{output.title}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{output.path}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={output.doc_url ?? toDocsUrl(output.path)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{truncate(output.content, 800)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="pb-2 pt-3">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{output.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{output.type === "no_results" && (
|
||||
<div>
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.suggestions && output.suggestions.length > 0 && (
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
|
||||
{output.suggestions.slice(0, 5).map((s) => (
|
||||
<li key={s}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div>
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{output.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user