feat: refine copilot tools styles

This commit is contained in:
Lluis Agusti
2026-02-09 18:44:25 +08:00
parent a350d6d1b7
commit fba027b7a4
31 changed files with 2155 additions and 504 deletions

View File

@@ -41,8 +41,9 @@ export const ChatContainer = ({
isLoading={isLoadingSession}
/>
<motion.div
layoutId={inputLayoutId}
transition={{ type: "spring", bounce: 0.2, duration: 0.65 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="relative px-3 pb-2 pt-2"
>
<div className="pointer-events-none absolute left-0 right-0 top-[-18px] z-10 h-6 bg-gradient-to-b from-transparent to-[#f8f8f9]" />

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ai-elements/message";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { UIDataTypes, UIMessage, UITools, ToolUIPart } from "ai";
import { useEffect, useState } from "react";
import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks";
import { FindAgentsTool } from "../../tools/FindAgents/FindAgents";
import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs";
@@ -19,6 +20,23 @@ import { ViewAgentOutputTool } from "../../tools/ViewAgentOutput/ViewAgentOutput
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
const THINKING_PHRASES = [
"Thinking...",
"Considering this...",
"Working through this...",
"Analyzing your request...",
"Reasoning...",
"Looking into it...",
"Processing your request...",
"Mulling this over...",
"Piecing it together...",
"On it...",
];
function getRandomPhrase() {
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)];
}
interface ChatMessagesContainerProps {
messages: UIMessage<unknown, UIDataTypes, UITools>[];
status: string;
@@ -32,6 +50,27 @@ export const ChatMessagesContainer = ({
error,
isLoading,
}: ChatMessagesContainerProps) => {
const [thinkingPhrase, setThinkingPhrase] = useState(getRandomPhrase);
useEffect(() => {
if (status === "submitted") {
setThinkingPhrase(getRandomPhrase());
}
}, [status]);
const lastMessage = messages[messages.length - 1];
const lastAssistantHasVisibleContent =
lastMessage?.role === "assistant" &&
lastMessage.parts.some(
(p) =>
(p.type === "text" && p.text.trim().length > 0) ||
p.type.startsWith("tool-"),
);
const showThinking =
status === "submitted" ||
(status === "streaming" && !lastAssistantHasVisibleContent);
return (
<Conversation className="min-h-0 flex-1">
<ConversationContent className="gap-6 px-3 py-6">
@@ -40,94 +79,112 @@ export const ChatMessagesContainer = ({
<LoadingSpinner size="large" className="text-neutral-400" />
</div>
)}
{messages.map((message) => (
<Message from={message.role} key={message.id}>
<MessageContent
className={
"text-[1rem] leading-relaxed " +
"group-[.is-user]:rounded-xl group-[.is-user]:bg-purple-100 group-[.is-user]:px-3 group-[.is-user]:py-2.5 group-[.is-user]:text-slate-900 group-[.is-user]:[border-bottom-right-radius:0] " +
"group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900"
}
>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return (
<MessageResponse key={`${message.id}-${i}`}>
{part.text}
</MessageResponse>
);
case "tool-find_block":
return (
<FindBlocksTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-find_agent":
case "tool-find_library_agent":
return (
<FindAgentsTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-search_docs":
case "tool-get_doc_page":
return (
<SearchDocsTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-run_block":
return (
<RunBlockTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-run_agent":
case "tool-schedule_agent":
return (
<RunAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-create_agent":
return (
<CreateAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-edit_agent":
return (
<EditAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-view_agent_output":
return (
<ViewAgentOutputTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
default:
return null;
{messages.map((message, messageIndex) => {
const isLastAssistant =
messageIndex === messages.length - 1 &&
message.role === "assistant";
const messageHasVisibleContent = message.parts.some(
(p) =>
(p.type === "text" && p.text.trim().length > 0) ||
p.type.startsWith("tool-"),
);
return (
<Message from={message.role} key={message.id}>
<MessageContent
className={
"text-[1rem] leading-relaxed " +
"group-[.is-user]:rounded-xl group-[.is-user]:bg-purple-100 group-[.is-user]:px-3 group-[.is-user]:py-2.5 group-[.is-user]:text-slate-900 group-[.is-user]:[border-bottom-right-radius:0] " +
"group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900"
}
})}
</MessageContent>
</Message>
))}
{status === "submitted" && (
>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return (
<MessageResponse key={`${message.id}-${i}`}>
{part.text}
</MessageResponse>
);
case "tool-find_block":
return (
<FindBlocksTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-find_agent":
case "tool-find_library_agent":
return (
<FindAgentsTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-search_docs":
case "tool-get_doc_page":
return (
<SearchDocsTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-run_block":
return (
<RunBlockTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-run_agent":
case "tool-schedule_agent":
return (
<RunAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-create_agent":
return (
<CreateAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-edit_agent":
return (
<EditAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-view_agent_output":
return (
<ViewAgentOutputTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
default:
return null;
}
})}
{isLastAssistant &&
!messageHasVisibleContent &&
showThinking && (
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
{thinkingPhrase}
</span>
)}
</MessageContent>
</Message>
);
})}
{showThinking && lastMessage?.role !== "assistant" && (
<Message from="assistant">
<MessageContent className="text-[1rem] leading-relaxed">
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
Thinking...
{thinkingPhrase}
</span>
</MessageContent>
</Message>

View File

@@ -52,9 +52,9 @@ export function EmptySession({
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto bg-[#f8f8f9] px-0 py-5 md:px-6 md:py-10">
<motion.div
className="w-full max-w-3xl text-center"
initial={{ opacity: 0, y: 14, filter: "blur(6px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ type: "spring", bounce: 0.2, duration: 0.7 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className="mx-auto max-w-3xl">
<Text variant="h3" className="mb-1 !text-[1.375rem] text-zinc-700">

View File

@@ -0,0 +1,223 @@
import { Link } from "@/components/atoms/Link/Link";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
/* ------------------------------------------------------------------ */
/* Layout */
/* ------------------------------------------------------------------ */
export function ContentGrid({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return <div className={cn("grid gap-2", className)}>{children}</div>;
}
/* ------------------------------------------------------------------ */
/* Card */
/* ------------------------------------------------------------------ */
export function ContentCard({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={cn(
"rounded-lg bg-gradient-to-r from-purple-500/30 to-blue-500/30 p-[1px]",
className,
)}
>
<div className="rounded-lg bg-neutral-100 p-3">{children}</div>
</div>
);
}
/** Flex row with a left content area (`children`) and an optional rightside `action`. */
export function ContentCardHeader({
children,
action,
className,
}: {
children: React.ReactNode;
action?: React.ReactNode;
className?: string;
}) {
return (
<div className={cn("flex items-start justify-between gap-2", className)}>
<div className="min-w-0">{children}</div>
{action}
</div>
);
}
export function ContentCardTitle({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<Text variant="body-medium" className={cn("truncate text-zinc-800", className)}>
{children}
</Text>
);
}
export function ContentCardSubtitle({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<Text variant="small" className={cn("mt-0.5 truncate text-zinc-800", className)}>
{children}
</Text>
);
}
export function ContentCardDescription({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<Text variant="small" className={cn("mt-2 text-zinc-800", className)}>{children}</Text>
);
}
/* ------------------------------------------------------------------ */
/* Text */
/* ------------------------------------------------------------------ */
export function ContentMessage({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return <Text variant="body" className={cn("text-zinc-800", className)}>{children}</Text>;
}
export function ContentHint({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<Text variant="small" className={cn("italic text-neutral-800", className)}>
{children}
</Text>
);
}
/* ------------------------------------------------------------------ */
/* Code / data */
/* ------------------------------------------------------------------ */
export function ContentCodeBlock({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<pre
className={cn(
"whitespace-pre-wrap rounded-lg border bg-black p-3 text-xs text-neutral-200",
className,
)}
>
{children}
</pre>
);
}
/* ------------------------------------------------------------------ */
/* Inline elements */
/* ------------------------------------------------------------------ */
export function ContentBadge({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<Text
variant="small"
as="span"
className={cn(
"shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-zinc-800",
className,
)}
>
{children}
</Text>
);
}
export function ContentLink({
href,
children,
className,
...rest
}: Omit<React.ComponentProps<typeof Link>, "className"> & {
className?: string;
}) {
return (
<Link
variant="primary"
isExternal
href={href}
className={cn("shrink-0 text-xs text-purple-500", className)}
{...rest}
>
{children}
</Link>
);
}
/* ------------------------------------------------------------------ */
/* Lists */
/* ------------------------------------------------------------------ */
export function ContentSuggestionsList({
items,
max = 5,
className,
}: {
items: string[];
max?: number;
className?: string;
}) {
if (items.length === 0) return null;
return (
<ul
className={cn(
"mt-2 list-disc space-y-1 pl-5 font-sans text-[0.75rem] leading-[1.125rem] text-zinc-800",
className,
)}
>
{items.slice(0, max).map((s) => (
<li key={s}>{s}</li>
))}
</ul>
);
}

View File

@@ -7,8 +7,9 @@ import { useId } from "react";
import { useToolAccordion } from "./useToolAccordion";
interface Props {
badgeText: string;
icon: React.ReactNode;
title: React.ReactNode;
titleClassName?: string;
description?: React.ReactNode;
children: React.ReactNode;
className?: string;
@@ -18,8 +19,9 @@ interface Props {
}
export function ToolAccordion({
badgeText,
icon,
title,
titleClassName,
description,
children,
className,
@@ -36,7 +38,12 @@ export function ToolAccordion({
});
return (
<div className={cn("mt-2 w-full rounded-lg border px-3 py-2", className)}>
<div
className={cn(
"mt-2 w-full rounded-lg border border-slate-200 bg-slate-100 px-3 py-2",
className,
)}
>
<button
type="button"
aria-expanded={isExpanded}
@@ -44,24 +51,22 @@ export function ToolAccordion({
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="px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
{badgeText}
<div className="flex min-w-0 items-center gap-3">
<span className="flex shrink-0 items-center text-gray-800">
{icon}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">
<p className={cn("truncate text-sm font-medium text-gray-800", titleClassName)}>
{title}
</p>
{description && (
<p className="truncate text-xs text-muted-foreground">
{description}
</p>
<p className="truncate text-xs text-slate-800">{description}</p>
)}
</div>
</div>
<CaretDownIcon
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
"h-4 w-4 shrink-0 text-slate-500 transition-transform",
isExpanded && "rotate-180",
)}
weight="bold"

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,17 @@
"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 {
ContentCardDescription,
ContentCardSubtitle,
ContentCodeBlock,
ContentGrid,
ContentHint,
ContentLink,
ContentMessage,
} from "../../components/ToolAccordion/AccordionContent";
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
import {
ClarificationQuestionsWidget,
@@ -20,6 +28,7 @@ import {
isOperationInProgressOutput,
isOperationPendingOutput,
isOperationStartedOutput,
AccordionIcon,
ToolIcon,
truncateText,
type CreateAgentToolOutput,
@@ -38,16 +47,18 @@ interface Props {
}
function getAccordionMeta(output: CreateAgentToolOutput): {
badgeText: string;
icon: React.ReactNode;
title: string;
description?: string;
} {
const icon = <AccordionIcon />;
if (isAgentSavedOutput(output)) {
return { badgeText: "Create agent", title: output.agent_name };
return { icon, title: output.agent_name };
}
if (isAgentPreviewOutput(output)) {
return {
badgeText: "Create agent",
icon,
title: output.agent_name,
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
};
@@ -55,7 +66,7 @@ function getAccordionMeta(output: CreateAgentToolOutput): {
if (isClarificationNeededOutput(output)) {
const questions = output.questions ?? [];
return {
badgeText: "Create agent",
icon,
title: "Needs clarification",
description: `${questions.length} question${questions.length === 1 ? "" : "s"}`,
};
@@ -65,9 +76,9 @@ function getAccordionMeta(output: CreateAgentToolOutput): {
isOperationPendingOutput(output) ||
isOperationInProgressOutput(output)
) {
return { badgeText: "Create agent", title: "Creating agent" };
return { icon, title: "Creating agent" };
}
return { badgeText: "Create agent", title: "Error" };
return { icon, title: "Error" };
}
export function CreateAgentTool({ part }: Props) {
@@ -117,64 +128,58 @@ export function CreateAgentTool({ part }: Props) {
>
{(isOperationStartedOutput(output) ||
isOperationPendingOutput(output)) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs text-muted-foreground">
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
<ContentCardSubtitle>
Operation: {output.operation_id}
</p>
<p className="text-xs italic text-muted-foreground">
</ContentCardSubtitle>
<ContentHint>
Check your library in a few minutes.
</p>
</div>
</ContentHint>
</ContentGrid>
)}
{isOperationInProgressOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs italic text-muted-foreground">
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
<ContentHint>
Please wait for the current operation to finish.
</p>
</div>
</ContentHint>
</ContentGrid>
)}
{isAgentSavedOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
<div className="flex flex-wrap gap-2">
<Link
href={output.library_agent_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
<ContentLink href={output.library_agent_link}>
Open in library
</Link>
<Link
href={output.agent_page_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
</ContentLink>
<ContentLink href={output.agent_page_link}>
Open in builder
</Link>
</ContentLink>
</div>
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{truncateText(
formatMaybeJson({ agent_id: output.agent_id }),
800,
)}
</pre>
</div>
</ContentCodeBlock>
</ContentGrid>
)}
{isAgentPreviewOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.description?.trim() && (
<p className="text-xs text-muted-foreground">
<ContentCardDescription>
{output.description}
</p>
</ContentCardDescription>
)}
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{truncateText(formatMaybeJson(output.agent_json), 1600)}
</pre>
</div>
</ContentCodeBlock>
</ContentGrid>
)}
{isClarificationNeededOutput(output) && (
@@ -197,19 +202,19 @@ export function CreateAgentTool({ part }: Props) {
)}
{isErrorOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{formatMaybeJson(output.error)}
</pre>
</ContentCodeBlock>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{formatMaybeJson(output.details)}
</pre>
</ContentCodeBlock>
)}
</div>
</ContentGrid>
)}
</ToolAccordion>
)}

View File

@@ -1,5 +1,3 @@
import type { ToolUIPart } from "ai";
import { PlusIcon } from "@phosphor-icons/react";
import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentPreviewResponse";
import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
@@ -8,6 +6,8 @@ import type { OperationInProgressResponse } from "@/app/api/__generated__/models
import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import { PlusCircleIcon, PlusIcon } from "@phosphor-icons/react";
import type { ToolUIPart } from "ai";
export type CreateAgentToolOutput =
| OperationStartedResponse
@@ -165,6 +165,10 @@ export function ToolIcon({
);
}
export function AccordionIcon() {
return <PlusCircleIcon size={32} weight="light" />;
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {

View File

@@ -1,9 +1,17 @@
"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 {
ContentCardDescription,
ContentCardSubtitle,
ContentCodeBlock,
ContentGrid,
ContentHint,
ContentLink,
ContentMessage,
} from "../../components/ToolAccordion/AccordionContent";
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
import {
ClarificationQuestionsWidget,
@@ -20,6 +28,7 @@ import {
isOperationInProgressOutput,
isOperationPendingOutput,
isOperationStartedOutput,
AccordionIcon,
ToolIcon,
truncateText,
type EditAgentToolOutput,
@@ -38,16 +47,18 @@ interface Props {
}
function getAccordionMeta(output: EditAgentToolOutput): {
badgeText: string;
icon: React.ReactNode;
title: string;
description?: string;
} {
const icon = <AccordionIcon />;
if (isAgentSavedOutput(output)) {
return { badgeText: "Edit agent", title: output.agent_name };
return { icon, title: output.agent_name };
}
if (isAgentPreviewOutput(output)) {
return {
badgeText: "Edit agent",
icon,
title: output.agent_name,
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
};
@@ -55,7 +66,7 @@ function getAccordionMeta(output: EditAgentToolOutput): {
if (isClarificationNeededOutput(output)) {
const questions = output.questions ?? [];
return {
badgeText: "Edit agent",
icon,
title: "Needs clarification",
description: `${questions.length} question${questions.length === 1 ? "" : "s"}`,
};
@@ -65,9 +76,9 @@ function getAccordionMeta(output: EditAgentToolOutput): {
isOperationPendingOutput(output) ||
isOperationInProgressOutput(output)
) {
return { badgeText: "Edit agent", title: "Editing agent" };
return { icon, title: "Editing agent" };
}
return { badgeText: "Edit agent", title: "Error" };
return { icon, title: "Error" };
}
export function EditAgentTool({ part }: Props) {
@@ -117,64 +128,58 @@ export function EditAgentTool({ part }: Props) {
>
{(isOperationStartedOutput(output) ||
isOperationPendingOutput(output)) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs text-muted-foreground">
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
<ContentCardSubtitle>
Operation: {output.operation_id}
</p>
<p className="text-xs italic text-muted-foreground">
</ContentCardSubtitle>
<ContentHint>
Check your library in a few minutes.
</p>
</div>
</ContentHint>
</ContentGrid>
)}
{isOperationInProgressOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs italic text-muted-foreground">
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
<ContentHint>
Please wait for the current operation to finish.
</p>
</div>
</ContentHint>
</ContentGrid>
)}
{isAgentSavedOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
<div className="flex flex-wrap gap-2">
<Link
href={output.library_agent_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
<ContentLink href={output.library_agent_link}>
Open in library
</Link>
<Link
href={output.agent_page_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
</ContentLink>
<ContentLink href={output.agent_page_link}>
Open in builder
</Link>
</ContentLink>
</div>
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{truncateText(
formatMaybeJson({ agent_id: output.agent_id }),
800,
)}
</pre>
</div>
</ContentCodeBlock>
</ContentGrid>
)}
{isAgentPreviewOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.description?.trim() && (
<p className="text-xs text-muted-foreground">
<ContentCardDescription>
{output.description}
</p>
</ContentCardDescription>
)}
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{truncateText(formatMaybeJson(output.agent_json), 1600)}
</pre>
</div>
</ContentCodeBlock>
</ContentGrid>
)}
{isClarificationNeededOutput(output) && (
@@ -197,19 +202,19 @@ export function EditAgentTool({ part }: Props) {
)}
{isErrorOutput(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{formatMaybeJson(output.error)}
</pre>
</ContentCodeBlock>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{formatMaybeJson(output.details)}
</pre>
</ContentCodeBlock>
)}
</div>
</ContentGrid>
)}
</ToolAccordion>
)}

View File

@@ -1,5 +1,3 @@
import type { ToolUIPart } from "ai";
import { PencilLineIcon } from "@phosphor-icons/react";
import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentPreviewResponse";
import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
@@ -8,6 +6,8 @@ import type { OperationInProgressResponse } from "@/app/api/__generated__/models
import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import { NotePencilIcon, PencilLineIcon } from "@phosphor-icons/react";
import type { ToolUIPart } from "ai";
export type EditAgentToolOutput =
| OperationStartedResponse
@@ -165,6 +165,10 @@ export function ToolIcon({
);
}
export function AccordionIcon() {
return <NotePencilIcon size={32} weight="light" />;
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {

View File

@@ -1,9 +1,19 @@
"use client";
import { ToolUIPart } from "ai";
import Link from "next/link";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import {
ContentBadge,
ContentCard,
ContentCardDescription,
ContentCardHeader,
ContentCardTitle,
ContentGrid,
ContentLink,
} from "../../components/ToolAccordion/AccordionContent";
import {
AccordionIcon,
getAgentHref,
getAnimationText,
getFindAgentsOutput,
@@ -12,7 +22,6 @@ import {
isErrorOutput,
ToolIcon,
} from "./helpers";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
export interface FindAgentsToolPart {
type: string;
@@ -77,11 +86,11 @@ export function FindAgentsTool({ part }: Props) {
{hasAgents && agentsFoundOutput && (
<ToolAccordion
badgeText={sourceLabel}
icon={<AccordionIcon toolType={part.type} />}
title="Agent results"
description={accordionDescription}
>
<div className="grid gap-2 sm:grid-cols-2">
<ContentGrid className="sm:grid-cols-2">
{agentsFoundOutput.agents.map((agent) => {
const href = getAgentHref(agent);
const agentSource =
@@ -91,39 +100,26 @@ export function FindAgentsTool({ part }: Props) {
? "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>
<ContentCard key={agent.id}>
<ContentCardHeader
action={
href ? <ContentLink href={href}>Open</ContentLink> : null
}
>
<div className="flex items-center gap-2">
<ContentCardTitle>{agent.name}</ContentCardTitle>
{agentSource && (
<ContentBadge>{agentSource}</ContentBadge>
)}
</div>
{href && (
<Link
href={href}
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open
</Link>
)}
</div>
</div>
<ContentCardDescription className="mt-1 line-clamp-2">
{agent.description}
</ContentCardDescription>
</ContentCardHeader>
</ContentCard>
);
})}
</div>
</ContentGrid>
</ToolAccordion>
)}
</div>

View File

@@ -1,10 +1,15 @@
import { ToolUIPart } from "ai";
import { MagnifyingGlassIcon, SquaresFourIcon } from "@phosphor-icons/react";
import type { AgentInfo } from "@/app/api/__generated__/models/agentInfo";
import type { AgentsFoundResponse } from "@/app/api/__generated__/models/agentsFoundResponse";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import {
FolderOpenIcon,
MagnifyingGlassIcon,
SquaresFourIcon,
StorefrontIcon,
} from "@phosphor-icons/react";
import { ToolUIPart } from "ai";
export interface FindAgentInput {
query: string;
@@ -174,3 +179,9 @@ export function ToolIcon({
/>
);
}
export function AccordionIcon({ toolType }: { toolType?: FindAgentsToolType }) {
const { source } = getSourceLabelFromToolType(toolType);
const IconComponent = source === "library" ? FolderOpenIcon : StorefrontIcon;
return <IconComponent size={32} weight="light" />;
}

View File

@@ -2,11 +2,16 @@
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import {
ContentCard,
ContentCardDescription,
ContentCardTitle,
} from "../../components/ToolAccordion/AccordionContent";
import type { BlockListResponse } from "@/app/api/__generated__/models/blockListResponse";
import type { BlockInfoSummary } from "@/app/api/__generated__/models/blockInfoSummary";
import { ToolUIPart } from "ai";
import { HorizontalScroll } from "@/app/(platform)/build/components/NewControlPanel/NewBlockMenu/HorizontalScroll";
import { getAnimationText, parseOutput, ToolIcon } from "./helpers";
import { AccordionIcon, getAnimationText, parseOutput, ToolIcon } from "./helpers";
export interface FindBlockInput {
query: string;
@@ -30,14 +35,12 @@ interface Props {
function BlockCard({ block }: { block: BlockInfoSummary }) {
return (
<div className="w-48 shrink-0 rounded-2xl border bg-background p-3">
<p className="truncate text-sm font-medium text-foreground">
{block.name}
</p>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
<ContentCard className="w-48 shrink-0">
<ContentCardTitle>{block.name}</ContentCardTitle>
<ContentCardDescription className="mt-1 line-clamp-2">
{block.description}
</p>
</div>
</ContentCardDescription>
</ContentCard>
);
}
@@ -68,7 +71,7 @@ export function FindBlocksTool({ part }: Props) {
{hasBlocks && parsed && (
<ToolAccordion
badgeText="Blocks"
icon={<AccordionIcon />}
title="Block results"
description={accordionDescription}
>

View File

@@ -1,8 +1,7 @@
import { ToolUIPart } from "ai";
import type { BlockListResponse } from "@/app/api/__generated__/models/blockListResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import { CubeIcon, PackageIcon } from "@phosphor-icons/react";
import { FindBlockInput, FindBlockToolPart } from "./FindBlocks";
import { PackageIcon } from "@phosphor-icons/react";
export function parseOutput(output: unknown): BlockListResponse | null {
if (!output) return null;
@@ -70,3 +69,7 @@ export function ToolIcon({
/>
);
}
export function AccordionIcon() {
return <CubeIcon size={32} weight="light" />;
}

View File

@@ -3,6 +3,7 @@
import type { ToolUIPart } from "ai";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import { ContentMessage } from "../../components/ToolAccordion/AccordionContent";
import {
getAccordionMeta,
getAnimationText,
@@ -80,7 +81,7 @@ export function RunAgentTool({ part }: Props) {
)}
{isRunAgentNeedLoginOutput(output) && (
<p className="text-sm text-foreground">{output.message}</p>
<ContentMessage>{output.message}</ContentMessage>
)}
{isRunAgentErrorOutput(output) && <ErrorCard output={output} />}

View File

@@ -1,11 +1,13 @@
"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 { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
import { buildInputSchema } from "./helpers";
interface Props {
@@ -38,21 +40,16 @@ export function AgentDetailsCard({ output }: Props) {
return (
<div className="grid gap-2">
<p className="text-sm text-foreground">
<ContentMessage>
Run this agent with example values or your own inputs.
</p>
</ContentMessage>
<div className="flex gap-2 pt-4">
<Button
variant="outline"
size="small"
className="w-fit"
onClick={handleRunWithExamples}
>
<Button size="small" className="w-fit" onClick={handleRunWithExamples}>
Run with example values
</Button>
<Button
variant="secondary"
variant="outline"
size="small"
className="w-fit"
onClick={() => setShowInputForm((prev) => !prev)}
@@ -76,9 +73,9 @@ export function AgentDetailsCard({ output }: Props) {
style={{ willChange: "height, opacity, filter" }}
>
<div className="mt-4 rounded-2xl border bg-background p-3 pt-4">
<p className="text-sm font-medium text-foreground">
<Text variant="body-medium">
Enter your inputs
</p>
</Text>
<FormRenderer
jsonSchema={buildInputSchema(output.agent.inputs)!}
handleChange={(v) => setInputValues(v.formData ?? {})}

View File

@@ -1,6 +1,11 @@
"use client";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import {
ContentCodeBlock,
ContentGrid,
ContentMessage,
} from "../../../../components/ToolAccordion/AccordionContent";
import { formatMaybeJson } from "../../helpers";
interface Props {
@@ -9,18 +14,14 @@ interface Props {
export function ErrorCard({ output }: Props) {
return (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
<ContentCodeBlock>{formatMaybeJson(output.error)}</ContentCodeBlock>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
<ContentCodeBlock>{formatMaybeJson(output.details)}</ContentCodeBlock>
)}
</div>
</ContentGrid>
);
}

View File

@@ -3,6 +3,13 @@
import { useRouter } from "next/navigation";
import { Button } from "@/components/atoms/Button/Button";
import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse";
import {
ContentCard,
ContentCardDescription,
ContentCardSubtitle,
ContentCardTitle,
ContentGrid,
} from "../../../../components/ToolAccordion/AccordionContent";
interface Props {
output: ExecutionStartedResponse;
@@ -12,17 +19,11 @@ export function ExecutionStartedCard({ output }: Props) {
const router = useRouter();
return (
<div className="grid gap-2">
<div className="rounded-2xl border bg-background p-3">
<div className="min-w-0">
<p className="text-sm font-medium text-foreground">
Execution started
</p>
<p className="mt-0.5 truncate text-xs text-muted-foreground">
{output.execution_id}
</p>
<p className="mt-2 text-xs text-muted-foreground">{output.message}</p>
</div>
<ContentGrid>
<ContentCard>
<ContentCardTitle>Execution started</ContentCardTitle>
<ContentCardSubtitle>{output.execution_id}</ContentCardSubtitle>
<ContentCardDescription>{output.message}</ContentCardDescription>
{output.library_agent_link && (
<Button
variant="outline"
@@ -33,7 +34,7 @@ export function ExecutionStartedCard({ output }: Props) {
View Execution
</Button>
)}
</div>
</div>
</ContentCard>
</ContentGrid>
);
}

View File

@@ -6,6 +6,12 @@ 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 {
ContentBadge,
ContentCardDescription,
ContentCardTitle,
ContentMessage,
} from "../../../../components/ToolAccordion/AccordionContent";
import { coerceCredentialFields, coerceExpectedInputs } from "./helpers";
interface Props {
@@ -45,7 +51,7 @@ export function SetupRequirementsCard({ output }: Props) {
return (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentMessage>{output.message}</ContentMessage>
{credentialFields.length > 0 && (
<div className="rounded-2xl border bg-background p-3">
@@ -71,22 +77,22 @@ export function SetupRequirementsCard({ output }: Props) {
{expectedInputs.length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">Expected inputs</p>
<ContentCardTitle className="text-xs">Expected inputs</ContentCardTitle>
<div className="mt-2 grid gap-2">
{expectedInputs.map((input) => (
<div key={input.name} className="rounded-xl border p-2">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
<ContentCardTitle className="text-xs">
{input.title}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
</ContentCardTitle>
<ContentBadge>
{input.required ? "Required" : "Optional"}
</span>
</ContentBadge>
</div>
<p className="mt-1 text-xs text-muted-foreground">
<ContentCardDescription className="mt-1">
{input.name} &bull; {input.type}
{input.description ? ` \u2022 ${input.description}` : ""}
</p>
</ContentCardDescription>
</div>
))}
</div>

View File

@@ -1,11 +1,15 @@
import type { ToolUIPart } from "ai";
import { PlayIcon } from "@phosphor-icons/react";
import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentDetailsResponse";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse";
import type { NeedLoginResponse } from "@/app/api/__generated__/models/needLoginResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
import {
PlayIcon,
RocketLaunchIcon,
WarningDiamondIcon,
} from "@phosphor-icons/react";
import type { ToolUIPart } from "ai";
export interface RunAgentInput {
username_agent_slug?: string;
@@ -111,12 +115,6 @@ function getAgentIdentifierText(
return null;
}
function getExecutionModeText(input: RunAgentInput | undefined): string | null {
if (!input) return null;
const isSchedule = Boolean(input.schedule_name?.trim() || input.cron?.trim());
return isSchedule ? "Scheduled run" : "Run";
}
export function getAnimationText(part: {
state: ToolUIPart["state"];
input?: unknown;
@@ -166,21 +164,22 @@ export function ToolIcon({
isStreaming?: boolean;
isError?: boolean;
}) {
if (isError) {
return <WarningDiamondIcon size={14} weight="regular" className="text-red-500" />;
}
return (
<PlayIcon
size={14}
weight="regular"
className={
isError
? "text-red-500"
: isStreaming
? "text-neutral-500"
: "text-neutral-400"
}
className={isStreaming ? "text-neutral-500" : "text-neutral-400"}
/>
);
}
export function AccordionIcon() {
return <RocketLaunchIcon size={28} weight="light" />;
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
@@ -190,19 +189,21 @@ export function formatMaybeJson(value: unknown): string {
}
}
export function getAccordionMeta(output: RunAgentToolOutput): {
badgeText: string;
icon: React.ReactNode;
title: string;
titleClassName?: string;
description?: string;
} {
const icon = <AccordionIcon />;
if (isRunAgentExecutionStartedOutput(output)) {
const statusText =
typeof output.status === "string" && output.status.trim()
? output.status.trim()
: "started";
return {
badgeText: "Run agent",
icon,
title: output.graph_name,
description: `Status: ${statusText}`,
};
@@ -210,7 +211,7 @@ export function getAccordionMeta(output: RunAgentToolOutput): {
if (isRunAgentAgentDetailsOutput(output)) {
return {
badgeText: "Run agent",
icon,
title: output.agent.name,
description: "Inputs required",
};
@@ -224,7 +225,7 @@ export function getAccordionMeta(output: RunAgentToolOutput): {
>,
).length;
return {
badgeText: "Run agent",
icon,
title: output.setup_info.agent_name,
description:
missingCredsCount > 0
@@ -234,8 +235,12 @@ export function getAccordionMeta(output: RunAgentToolOutput): {
}
if (isRunAgentNeedLoginOutput(output)) {
return { badgeText: "Run agent", title: "Sign in required" };
return { icon, title: "Sign in required" };
}
return { badgeText: "Run agent", title: "Error" };
}
return {
icon: <WarningDiamondIcon size={28} weight="light" className="text-red-500" />,
title: "Error",
titleClassName: "text-red-500",
};
}

View File

@@ -3,6 +3,9 @@
import type { ToolUIPart } from "ai";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import { BlockOutputCard } from "./components/BlockOutputCard/BlockOutputCard";
import { ErrorCard } from "./components/ErrorCard/ErrorCard";
import { SetupRequirementsCard } from "./components/SetupRequirementsCard/SetupRequirementsCard";
import {
getAccordionMeta,
getAnimationText,
@@ -11,11 +14,7 @@ import {
isRunBlockErrorOutput,
isRunBlockSetupRequirementsOutput,
ToolIcon,
type RunBlockToolOutput,
} from "./helpers";
import { BlockOutputCard } from "./components/BlockOutputCard/BlockOutputCard";
import { SetupRequirementsCard } from "./components/SetupRequirementsCard/SetupRequirementsCard";
import { ErrorCard } from "./components/ErrorCard/ErrorCard";
export interface RunBlockToolPart {
type: string;

View File

@@ -4,6 +4,13 @@ import React, { useState } from "react";
import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
import { Button } from "@/components/atoms/Button/Button";
import type { BlockOutputResponse } from "@/app/api/__generated__/models/blockOutputResponse";
import {
ContentBadge,
ContentCard,
ContentCardTitle,
ContentGrid,
ContentMessage,
} from "../../../../components/ToolAccordion/AccordionContent";
import { formatMaybeJson } from "../../helpers";
interface Props {
@@ -103,14 +110,12 @@ function OutputKeySection({
const visibleItems = expanded ? items : items.slice(0, COLLAPSED_LIMIT);
return (
<div className="rounded-2xl border bg-background p-3">
<ContentCard>
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
{outputKey}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
<ContentCardTitle className="text-xs">{outputKey}</ContentCardTitle>
<ContentBadge>
{items.length} item{items.length === 1 ? "" : "s"}
</span>
</ContentBadge>
</div>
{mediaContent || (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
@@ -127,18 +132,18 @@ function OutputKeySection({
{expanded ? "Show less" : `Show all ${items.length} items`}
</Button>
)}
</div>
</ContentCard>
);
}
export function BlockOutputCard({ output }: Props) {
return (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{Object.entries(output.outputs ?? {}).map(([key, items]) => (
<OutputKeySection key={key} outputKey={key} items={items} />
))}
</div>
</ContentGrid>
);
}

View File

@@ -1,6 +1,11 @@
"use client";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import {
ContentCodeBlock,
ContentGrid,
ContentMessage,
} from "../../../../components/ToolAccordion/AccordionContent";
import { formatMaybeJson } from "../../helpers";
interface Props {
@@ -9,18 +14,14 @@ interface Props {
export function ErrorCard({ output }: Props) {
return (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
<ContentCodeBlock>{formatMaybeJson(output.error)}</ContentCodeBlock>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
<ContentCodeBlock>{formatMaybeJson(output.details)}</ContentCodeBlock>
)}
</div>
</ContentGrid>
);
}

View File

@@ -1,17 +1,24 @@
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
import {
ContentBadge,
ContentCardDescription,
ContentCardTitle,
ContentMessage,
} from "../../../../components/ToolAccordion/AccordionContent";
import {
buildExpectedInputsSchema,
coerceCredentialFields,
coerceExpectedInputs,
buildExpectedInputsSchema,
} from "./helpers";
interface Props {
@@ -69,7 +76,7 @@ export function SetupRequirementsCard({ output }: Props) {
return (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentMessage>{output.message}</ContentMessage>
{credentialFields.length > 0 && (
<div className="rounded-2xl border bg-background p-3">
@@ -96,7 +103,7 @@ export function SetupRequirementsCard({ output }: Props) {
{inputSchema && (
<div className="flex gap-2 pt-2">
<Button
variant="secondary"
variant="outline"
size="small"
className="w-fit"
onClick={() => setShowInputForm((prev) => !prev)}
@@ -121,9 +128,7 @@ export function SetupRequirementsCard({ output }: Props) {
style={{ willChange: "height, opacity, filter" }}
>
<div className="rounded-2xl border bg-background p-3 pt-4">
<p className="text-sm font-medium text-foreground">
Block inputs
</p>
<Text variant="body-medium">Block inputs</Text>
<FormRenderer
jsonSchema={inputSchema}
handleChange={(v) => setInputValues(v.formData ?? {})}
@@ -164,22 +169,24 @@ export function SetupRequirementsCard({ output }: Props) {
{expectedInputs.length > 0 && !inputSchema && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">Expected inputs</p>
<ContentCardTitle className="text-xs">
Expected inputs
</ContentCardTitle>
<div className="mt-2 grid gap-2">
{expectedInputs.map((input) => (
<div key={input.name} className="rounded-xl border p-2">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
<ContentCardTitle className="text-xs">
{input.title}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
</ContentCardTitle>
<ContentBadge>
{input.required ? "Required" : "Optional"}
</span>
</ContentBadge>
</div>
<p className="mt-1 text-xs text-muted-foreground">
<ContentCardDescription className="mt-1">
{input.name} &bull; {input.type}
{input.description ? ` \u2022 ${input.description}` : ""}
</p>
</ContentCardDescription>
</div>
))}
</div>

View File

@@ -1,9 +1,13 @@
import type { ToolUIPart } from "ai";
import { PlayIcon } from "@phosphor-icons/react";
import type { BlockOutputResponse } from "@/app/api/__generated__/models/blockOutputResponse";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
import {
PlayCircleIcon,
PlayIcon,
WarningDiamondIcon,
} from "@phosphor-icons/react";
import type { ToolUIPart } from "ai";
export interface RunBlockInput {
block_id?: string;
@@ -109,21 +113,22 @@ export function ToolIcon({
isStreaming?: boolean;
isError?: boolean;
}) {
if (isError) {
return <WarningDiamondIcon size={14} weight="regular" className="text-red-500" />;
}
return (
<PlayIcon
size={14}
weight="regular"
className={
isError
? "text-red-500"
: isStreaming
? "text-neutral-500"
: "text-neutral-400"
}
className={isStreaming ? "text-neutral-500" : "text-neutral-400"}
/>
);
}
export function AccordionIcon() {
return <PlayCircleIcon size={32} weight="light" />;
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
@@ -133,17 +138,18 @@ export function formatMaybeJson(value: unknown): string {
}
}
export function getAccordionMeta(output: RunBlockToolOutput): {
badgeText: string;
icon: React.ReactNode;
title: string;
titleClassName?: string;
description?: string;
} {
const icon = <AccordionIcon />;
if (isRunBlockBlockOutput(output)) {
const keys = Object.keys(output.outputs ?? {});
return {
badgeText: "Run block",
icon,
title: output.block_name,
description:
keys.length > 0
@@ -160,7 +166,7 @@ export function getAccordionMeta(output: RunBlockToolOutput): {
>,
).length;
return {
badgeText: "Run block",
icon,
title: output.setup_info.agent_name,
description:
missingCredsCount > 0
@@ -169,5 +175,9 @@ export function getAccordionMeta(output: RunBlockToolOutput): {
};
}
return { badgeText: "Run block", title: "Error" };
return {
icon: <WarningDiamondIcon size={32} weight="light" className="text-red-500" />,
title: "Error",
titleClassName: "text-red-500",
};
}

View File

@@ -1,21 +1,33 @@
"use client";
import type { ToolUIPart } from "ai";
import Link from "next/link";
import { useMemo } from "react";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import {
ContentCard,
ContentCardDescription,
ContentCardHeader,
ContentCardSubtitle,
ContentCardTitle,
ContentGrid,
ContentLink,
ContentMessage,
ContentSuggestionsList,
} from "../../components/ToolAccordion/AccordionContent";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import {
AccordionIcon,
getAnimationText,
getDocsToolOutput,
getDocsToolTitle,
getToolLabel,
getAnimationText,
isDocPageOutput,
isDocSearchResultsOutput,
isErrorOutput,
isNoResultsOutput,
ToolIcon,
toDocsUrl,
ToolIcon,
type DocsToolType,
} from "./helpers";
@@ -97,96 +109,73 @@ export function SearchDocsTool({ part }: Props) {
{hasExpandableContent && normalized && (
<ToolAccordion
badgeText={normalized.label}
icon={<AccordionIcon toolType={part.type} />}
title={normalized.title}
description={accordionDescription}
>
{docSearchOutput && (
<div className="grid gap-2">
<ContentGrid>
{docSearchOutput.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>
<ContentCard key={r.path}>
<ContentCardHeader
action={<ContentLink href={href}>Open</ContentLink>}
>
<ContentCardTitle>{r.title}</ContentCardTitle>
<ContentCardSubtitle>
{r.path}
{r.section ? `${r.section}` : ""}
</ContentCardSubtitle>
<ContentCardDescription>
{truncate(r.snippet, 240)}
</ContentCardDescription>
</ContentCardHeader>
</ContentCard>
);
})}
</div>
</ContentGrid>
)}
{docPageOutput && (
<div>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">
{docPageOutput.title}
</p>
<p className="mt-0.5 truncate text-xs text-muted-foreground">
{docPageOutput.path}
</p>
</div>
<Link
href={docPageOutput.doc_url ?? toDocsUrl(docPageOutput.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">
<ContentCardHeader
action={
<ContentLink
href={
docPageOutput.doc_url ?? toDocsUrl(docPageOutput.path)
}
>
Open
</ContentLink>
}
>
<ContentCardTitle>{docPageOutput.title}</ContentCardTitle>
<ContentCardSubtitle>{docPageOutput.path}</ContentCardSubtitle>
</ContentCardHeader>
<ContentCardDescription className="whitespace-pre-wrap">
{truncate(docPageOutput.content, 800)}
</p>
</ContentCardDescription>
</div>
)}
{noResultsOutput && (
<div>
<p className="text-sm text-foreground">
{noResultsOutput.message}
</p>
<ContentMessage>{noResultsOutput.message}</ContentMessage>
{noResultsOutput.suggestions &&
noResultsOutput.suggestions.length > 0 && (
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{noResultsOutput.suggestions.slice(0, 5).map((s) => (
<li key={s}>{s}</li>
))}
</ul>
<ContentSuggestionsList items={noResultsOutput.suggestions} />
)}
</div>
)}
{errorOutput && (
<div>
<p className="text-sm text-foreground">{errorOutput.message}</p>
<ContentMessage>{errorOutput.message}</ContentMessage>
{errorOutput.error && (
<p className="mt-2 text-xs text-muted-foreground">
<ContentCardDescription>
{errorOutput.error}
</p>
</ContentCardDescription>
)}
</div>
)}

View File

@@ -1,10 +1,14 @@
import { ToolUIPart } from "ai";
import { FileMagnifyingGlassIcon, FileTextIcon } from "@phosphor-icons/react";
import type { DocPageResponse } from "@/app/api/__generated__/models/docPageResponse";
import type { DocSearchResultsResponse } from "@/app/api/__generated__/models/docSearchResultsResponse";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import {
ArticleIcon,
FileMagnifyingGlassIcon,
FileTextIcon,
} from "@phosphor-icons/react";
import { ToolUIPart } from "ai";
export interface SearchDocsInput {
query: string;
@@ -197,6 +201,12 @@ export function ToolIcon({
);
}
export function AccordionIcon({ toolType }: { toolType: DocsToolType }) {
const IconComponent =
toolType === "tool-get_doc_page" ? ArticleIcon : FileTextIcon;
return <IconComponent size={32} weight="light" />;
}
export function toDocsUrl(path: string): string {
const urlPath = path.includes(".")
? path.slice(0, path.lastIndexOf("."))

View File

@@ -1,11 +1,22 @@
"use client";
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 {
ContentBadge,
ContentCard,
ContentCardHeader,
ContentCardSubtitle,
ContentCardTitle,
ContentCodeBlock,
ContentGrid,
ContentLink,
ContentMessage,
ContentSuggestionsList,
} from "../../components/ToolAccordion/AccordionContent";
import {
formatMaybeJson,
getAnimationText,
@@ -13,6 +24,7 @@ import {
isAgentOutputResponse,
isErrorResponse,
isNoResultsResponse,
AccordionIcon,
ToolIcon,
type ViewAgentOutputToolOutput,
} from "./helpers";
@@ -30,22 +42,24 @@ interface Props {
}
function getAccordionMeta(output: ViewAgentOutputToolOutput): {
badgeText: string;
icon: React.ReactNode;
title: string;
description?: string;
} {
const icon = <AccordionIcon />;
if (isAgentOutputResponse(output)) {
const status = output.execution?.status;
return {
badgeText: "Agent output",
icon,
title: output.agent_name,
description: status ? `Status: ${status}` : output.message,
};
}
if (isNoResultsResponse(output)) {
return { badgeText: "Agent output", title: "No results" };
return { icon, title: "No results" };
}
return { badgeText: "Agent output", title: "Error" };
return { icon, title: "Error" };
}
function resolveWorkspaceUrl(src: string): string {
@@ -157,111 +171,106 @@ export function ViewAgentOutputTool({ part }: Props) {
{hasExpandableContent && output && (
<ToolAccordion {...getAccordionMeta(output)}>
{isAgentOutputResponse(output) && (
<div className="grid gap-2">
<div className="flex items-start justify-between gap-3">
<p className="text-sm text-foreground">{output.message}</p>
{output.library_agent_link && (
<Link
href={output.library_agent_link}
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open
</Link>
)}
</div>
<ContentGrid>
<ContentCardHeader
className="gap-3"
action={
output.library_agent_link ? (
<ContentLink href={output.library_agent_link}>
Open
</ContentLink>
) : null
}
>
<ContentMessage>{output.message}</ContentMessage>
</ContentCardHeader>
{output.execution ? (
<div className="grid gap-2">
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
<ContentGrid>
<ContentCard>
<ContentCardTitle className="text-xs">
Execution
</p>
<p className="mt-1 truncate text-xs text-muted-foreground">
</ContentCardTitle>
<ContentCardSubtitle className="mt-1">
{output.execution.execution_id}
</p>
<p className="mt-1 text-xs text-muted-foreground">
</ContentCardSubtitle>
<ContentCardSubtitle className="mt-1">
Status: {output.execution.status}
</p>
</div>
</ContentCardSubtitle>
</ContentCard>
{output.execution.inputs_summary && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
<ContentCard>
<ContentCardTitle className="text-xs">
Inputs summary
</p>
</ContentCardTitle>
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
{formatMaybeJson(output.execution.inputs_summary)}
</pre>
</div>
</ContentCard>
)}
{Object.entries(output.execution.outputs ?? {}).map(
([key, items]) => {
const mediaContent = renderOutputValue(items);
return (
<div
key={key}
className="rounded-2xl border bg-background p-3"
>
<ContentCard key={key}>
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
<ContentCardTitle className="text-xs">
{key}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
</ContentCardTitle>
<ContentBadge>
{items.length} item
{items.length === 1 ? "" : "s"}
</span>
</ContentBadge>
</div>
{mediaContent || (
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
{formatMaybeJson(items.slice(0, 3))}
</pre>
)}
</div>
</ContentCard>
);
},
)}
</div>
</ContentGrid>
) : (
<div className="rounded-2xl border bg-background p-3">
<p className="text-sm text-foreground">
No execution selected.
</p>
<p className="mt-1 text-xs text-muted-foreground">
<ContentCard>
<ContentMessage>No execution selected.</ContentMessage>
<ContentCardSubtitle className="mt-1">
Try asking for a specific run or execution_id.
</p>
</div>
</ContentCardSubtitle>
</ContentCard>
)}
</div>
</ContentGrid>
)}
{isNoResultsResponse(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.suggestions && output.suggestions.length > 0 && (
<ul className="mt-1 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>
<ContentSuggestionsList
items={output.suggestions}
className="mt-1"
/>
)}
</div>
</ContentGrid>
)}
{isErrorResponse(output) && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<ContentGrid>
<ContentMessage>{output.message}</ContentMessage>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{formatMaybeJson(output.error)}
</pre>
</ContentCodeBlock>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
<ContentCodeBlock>
{formatMaybeJson(output.details)}
</pre>
</ContentCodeBlock>
)}
</div>
</ContentGrid>
)}
</ToolAccordion>
)}

View File

@@ -1,9 +1,9 @@
import type { ToolUIPart } from "ai";
import { EyeIcon } from "@phosphor-icons/react";
import type { AgentOutputResponse } from "@/app/api/__generated__/models/agentOutputResponse";
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse";
import { ResponseType } from "@/app/api/__generated__/models/responseType";
import { EyeIcon, MonitorIcon } from "@phosphor-icons/react";
import type { ToolUIPart } from "ai";
export interface ViewAgentOutputInput {
agent_name?: string;
@@ -144,6 +144,10 @@ export function ToolIcon({
);
}
export function AccordionIcon() {
return <MonitorIcon size={32} weight="light" />;
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {

View File

@@ -307,7 +307,7 @@ export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
className={cn(
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_pre]:!bg-white",
className,
)}
plugins={{ code, mermaid, math, cjk }}

View File

@@ -1,4 +1,5 @@
import React from "react";
import { cn } from "@/lib/utils";
import { As, Variant, variantElementMap, variants } from "./helpers";
type CustomProps = {
@@ -22,7 +23,7 @@ export function Text({
}: TextProps) {
const variantClasses = variants[size || variant] || variants.body;
const Element = outerAs || variantElementMap[variant];
const combinedClassName = `${variantClasses} ${className}`.trim();
const combinedClassName = cn(variantClasses, className);
return React.createElement(
Element,