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 a36d7e02ba..936d2e215f 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 @@ -13,6 +13,7 @@ import { MessageSquareIcon } from "lucide-react"; import { UIMessage, UIDataTypes, UITools, ToolUIPart } from "ai"; import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks"; import { FindAgentsTool } from "../../tools/FindAgents/FindAgents"; +import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs"; interface ChatMessagesContainerProps { messages: UIMessage[]; @@ -70,6 +71,14 @@ export const ChatMessagesContainer = ({ part={part as ToolUIPart} /> ); + case "tool-search_docs": + case "tool-get_doc_page": + return ( + + ); default: return null; } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx new file mode 100644 index 0000000000..eee7e5a875 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/SearchDocs.tsx @@ -0,0 +1,209 @@ +"use client"; + +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 { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; +import { cn } from "@/lib/utils"; +import { + getDocsToolOutput, + getDocsToolTitle, + getToolLabel, + getAnimationText, + StateIcon, + toDocsUrl, + type DocsToolType, +} from "./helpers"; + +export interface DocsToolPart { + type: DocsToolType; + toolCallId: string; + state: ToolUIPart["state"]; + input?: unknown; + output?: unknown; +} + +interface Props { + part: DocsToolPart; +} + +function truncate(text: string, maxChars: number): string { + const trimmed = text.trim(); + if (trimmed.length <= maxChars) return trimmed; + return `${trimmed.slice(0, maxChars).trimEnd()}…`; +} + +export function SearchDocsTool({ part }: Props) { + const shouldReduceMotion = useReducedMotion(); + const [isExpanded, setIsExpanded] = useState(false); + + const output = getDocsToolOutput(part); + const text = getAnimationText(part); + + const normalized = useMemo(() => { + if (!output) return null; + const title = getDocsToolTitle(part.type, output); + const label = getToolLabel(part.type); + return { title, label }; + }, [output, part.type]); + + const isOutputAvailable = part.state === "output-available" && !!output; + + const hasExpandableContent = + isOutputAvailable && + ((output.type === "doc_search_results" && output.count > 0) || + output.type === "doc_page" || + output.type === "no_results" || + output.type === "error"); + + return ( +
+
+ + +
+ + {hasExpandableContent && normalized && ( +
+ + + + {isExpanded && ( + + {output.type === "doc_search_results" && ( +
+ {output.results.map((r) => { + const href = r.doc_url ?? toDocsUrl(r.path); + return ( +
+
+
+

+ {r.title} +

+

+ {r.path} + {r.section ? ` • ${r.section}` : ""} +

+

+ {truncate(r.snippet, 240)} +

+
+ + Open + +
+
+ ); + })} +
+ )} + + {output.type === "doc_page" && ( +
+
+
+

+ {output.title} +

+

+ {output.path} +

+
+ + Open + +
+

+ {truncate(output.content, 800)} +

+
+ )} + + {output.type === "no_results" && ( +
+

{output.message}

+ {output.suggestions && output.suggestions.length > 0 && ( +
    + {output.suggestions.slice(0, 5).map((s) => ( +
  • {s}
  • + ))} +
+ )} +
+ )} + + {output.type === "error" && ( +
+

{output.message}

+ {output.error && ( +

+ {output.error} +

+ )} +
+ )} +
+ )} +
+
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx new file mode 100644 index 0000000000..2c1583ce6e --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/tools/SearchDocs/helpers.tsx @@ -0,0 +1,207 @@ +import { ToolUIPart } from "ai"; +import { + CheckCircleIcon, + CircleNotchIcon, + XCircleIcon, +} from "@phosphor-icons/react"; + +export interface SearchDocsInput { + query: string; +} + +export interface GetDocPageInput { + path: string; +} + +export interface DocSearchResult { + title: string; + path: string; + section: string; + snippet: string; + score: number; + doc_url?: string | null; +} + +export interface DocSearchResultsOutput { + type: "doc_search_results"; + message: string; + session_id?: string; + results: DocSearchResult[]; + count: number; + query: string; +} + +export interface DocPageOutput { + type: "doc_page"; + message: string; + session_id?: string; + title: string; + path: string; + content: string; + doc_url?: string | null; +} + +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 DocsToolOutput = + | DocSearchResultsOutput + | DocPageOutput + | NoResultsOutput + | ErrorOutput; + +export type DocsToolType = "tool-search_docs" | "tool-get_doc_page" | string; + +export function getToolLabel(toolType: DocsToolType): string { + switch (toolType) { + case "tool-search_docs": + return "Docs"; + case "tool-get_doc_page": + return "Docs page"; + default: + return "Docs"; + } +} + +function parseOutput(output: unknown): DocsToolOutput | null { + if (!output) return null; + if (typeof output === "string") { + const trimmed = output.trim(); + if (!trimmed) return null; + try { + return JSON.parse(trimmed) as DocsToolOutput; + } catch { + return null; + } + } + if (typeof output === "object") { + return output as DocsToolOutput; + } + return null; +} + +export function getDocsToolOutput(part: unknown): DocsToolOutput | null { + if (!part || typeof part !== "object") return null; + return parseOutput((part as { output?: unknown }).output); +} + +export function getDocsToolTitle( + toolType: DocsToolType, + output: DocsToolOutput, +): string { + if (toolType === "tool-search_docs") { + if (output.type === "doc_search_results") return "Documentation results"; + if (output.type === "no_results") return "No documentation found"; + return "Documentation search error"; + } + + if (output.type === "doc_page") return "Documentation page"; + if (output.type === "no_results") return "No documentation found"; + return "Documentation page error"; +} + +export function getAnimationText(part: { + type: DocsToolType; + state: ToolUIPart["state"]; + input?: unknown; + output?: unknown; +}): string { + switch (part.type) { + case "tool-search_docs": { + switch (part.state) { + case "input-streaming": + return "Searching docs for you"; + case "input-available": { + const query = ( + part.input as SearchDocsInput | undefined + )?.query?.trim(); + return query ? `Searching docs for "${query}"` : "Searching docs"; + } + case "output-available": { + const output = parseOutput(part.output); + const query = ( + part.input as SearchDocsInput | undefined + )?.query?.trim(); + if (!output) return "Found documentation"; + if (output.type === "doc_search_results") { + const count = output.count ?? output.results.length; + return query + ? `Found ${count} doc result${count === 1 ? "" : "s"} for "${query}"` + : `Found ${count} doc result${count === 1 ? "" : "s"}`; + } + if (output.type === "no_results") { + return query ? `No docs found for "${query}"` : "No docs found"; + } + return "Error searching docs"; + } + case "output-error": + return "Error searching docs"; + default: + return "Processing"; + } + } + case "tool-get_doc_page": { + switch (part.state) { + case "input-streaming": + return "Loading documentation page"; + case "input-available": { + const path = ( + part.input as GetDocPageInput | undefined + )?.path?.trim(); + return path ? `Loading "${path}"` : "Loading documentation page"; + } + case "output-available": { + const output = parseOutput(part.output); + if (!output) return "Loaded documentation page"; + if (output.type === "doc_page") return `Loaded "${output.title}"`; + if (output.type === "no_results") + return "Documentation page not found"; + return "Error loading documentation page"; + } + case "output-error": + return "Error loading documentation page"; + default: + return "Processing"; + } + } + } + + return "Processing"; +} + +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; + } +} + +export function toDocsUrl(path: string): string { + const urlPath = path.includes(".") + ? path.slice(0, path.lastIndexOf(".")) + : path; + return `https://docs.agpt.co/${urlPath}`; +}