chore: more changes

This commit is contained in:
Lluis Agusti
2025-12-16 18:24:27 +01:00
parent 806e3b63d5
commit 87728ee085
6 changed files with 294 additions and 77 deletions

View File

@@ -5,10 +5,12 @@ import { AdminImpersonationBanner } from "./admin/components/AdminImpersonationB
export default function PlatformLayout({ children }: { children: ReactNode }) {
return (
<main className="flex h-screen w-full flex-col">
<Navbar />
<AdminImpersonationBanner />
<section className="flex-1">{children}</section>
<main className="flex h-screen w-full flex-row overflow-hidden">
<div className="flex min-w-0 flex-1 flex-col">
<Navbar />
<AdminImpersonationBanner />
<section className="flex-1 overflow-auto">{children}</section>
</div>
<ChatDrawer />
</main>
);

View File

@@ -38,36 +38,35 @@ export function ChatDrawer({ blurBackground = true }: ChatDrawerProps) {
direction="right"
modal={false}
>
<Drawer.Portal>
{blurBackground && isOpen && (
<div
onClick={close}
className="fixed inset-0 z-[45] cursor-pointer bg-black/10 backdrop-blur-sm animate-in fade-in-0"
style={{ pointerEvents: "auto" }}
/>
{blurBackground && isOpen && (
<div
onClick={close}
className="fixed inset-0 z-[45] cursor-pointer bg-black/10 backdrop-blur-sm animate-in fade-in-0"
style={{ pointerEvents: "auto" }}
/>
)}
<Drawer.Content
onClick={(e) => e.stopPropagation()}
onInteractOutside={blurBackground ? close : undefined}
className={cn(
"flex h-full w-1/2 flex-col border-l border-zinc-200 bg-white",
scrollbarStyles,
)}
<Drawer.Content
onClick={(e) => e.stopPropagation()}
onInteractOutside={blurBackground ? close : undefined}
className={cn(
"fixed right-0 top-0 z-50 flex h-full w-1/2 flex-col border-l border-zinc-200 bg-white",
scrollbarStyles,
)}
>
<Chat
headerTitle={
<Drawer.Title className="text-lg font-semibold">
AutoGPT Copilot
</Drawer.Title>
}
headerActions={
<button aria-label="Close" onClick={close} className="size-8">
<X width="1.25rem" height="1.25rem" />
</button>
}
/>
</Drawer.Content>
</Drawer.Portal>
style={{ position: "relative", zIndex: 50 }}
>
<Chat
headerTitle={
<Drawer.Title className="text-lg font-semibold">
AutoGPT Copilot
</Drawer.Title>
}
headerActions={
<button aria-label="Close" onClick={close} className="size-8">
<X width="1.25rem" height="1.25rem" />
</button>
}
/>
</Drawer.Content>
</Drawer.Root>
);
}

View File

@@ -5,10 +5,17 @@ import Avatar, {
AvatarFallback,
AvatarImage,
} from "@/components/atoms/Avatar/Avatar";
import { Button } from "@/components/atoms/Button/Button";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils";
import { CheckCircleIcon, RobotIcon } from "@phosphor-icons/react";
import { useCallback } from "react";
import {
ArrowClockwise,
CheckCircleIcon,
CheckIcon,
CopyIcon,
RobotIcon,
} from "@phosphor-icons/react";
import { useCallback, useState } from "react";
import { getToolActionPhrase } from "../../helpers";
import { AgentInputsSetup } from "../AgentInputsSetup/AgentInputsSetup";
import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
@@ -24,6 +31,7 @@ export interface ChatMessageProps {
onDismissLogin?: () => void;
onDismissCredentials?: () => void;
onSendMessage?: (content: string, isUserMessage?: boolean) => void;
agentOutput?: ChatMessageData;
}
export function ChatMessage({
@@ -31,8 +39,10 @@ export function ChatMessage({
className,
onDismissCredentials,
onSendMessage,
agentOutput,
}: ChatMessageProps) {
const { user } = useSupabase();
const [copied, setCopied] = useState(false);
const {
isUser,
isToolCall,
@@ -116,6 +126,23 @@ export function ChatMessage({
[onSendMessage, message],
);
const handleCopy = useCallback(async () => {
if (message.type !== "message") return;
try {
await navigator.clipboard.writeText(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error("Failed to copy:", error);
}
}, [message]);
const handleTryAgain = useCallback(() => {
if (message.type !== "message" || !onSendMessage) return;
onSendMessage(message.content, message.role === "user");
}, [message, onSendMessage]);
// Render inputs needed messages
if (isInputsNeeded && message.type === "inputs_needed") {
return (
@@ -196,13 +223,30 @@ export function ChatMessage({
);
}
// Render tool response messages
// Render tool response messages (but skip agent_output if it's being rendered inside assistant message)
if (
(isToolResponse && message.type === "tool_response") ||
message.type === "no_results" ||
message.type === "agent_carousel" ||
message.type === "execution_started"
) {
// Check if this is an agent_output that should be rendered inside assistant message
if (message.type === "tool_response" && message.result) {
let parsedResult: Record<string, unknown> | null = null;
try {
parsedResult =
typeof message.result === "string"
? JSON.parse(message.result)
: (message.result as Record<string, unknown>);
} catch {
parsedResult = null;
}
if (parsedResult?.type === "agent_output") {
// Skip rendering - this will be rendered inside the assistant message
return null;
}
}
return (
<div className={cn("px-4 py-2", className)}>
<ToolResponseMessage
@@ -240,7 +284,50 @@ export function ChatMessage({
>
<MessageBubble variant={isUser ? "user" : "assistant"}>
<MarkdownContent content={message.content} />
{agentOutput &&
agentOutput.type === "tool_response" &&
!isUser && (
<div className="mt-4">
<ToolResponseMessage
toolName={
agentOutput.toolName
? getToolActionPhrase(agentOutput.toolName)
: "Agent Output"
}
result={agentOutput.result}
/>
</div>
)}
</MessageBubble>
<div
className={cn(
"mt-1 flex gap-1",
isUser ? "justify-end" : "justify-start",
)}
>
{isUser && onSendMessage && (
<Button
variant="ghost"
size="icon"
onClick={handleTryAgain}
aria-label="Try again"
>
<ArrowClockwise className="size-3 text-neutral-500" />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={handleCopy}
aria-label="Copy message"
>
{copied ? (
<CheckIcon className="size-3 text-green-600" />
) : (
<CopyIcon className="size-3 text-neutral-500" />
)}
</Button>
</div>
</div>
{isUser && (

View File

@@ -38,13 +38,67 @@ export function MessageList({
>
<div className="mx-auto flex max-w-3xl flex-col py-4">
{/* Render all persisted messages */}
{messages.map((message, index) => (
<ChatMessage
key={index}
message={message}
onSendMessage={onSendMessage}
/>
))}
{messages.map((message, index) => {
// Check if current message is an agent_output tool_response
// and if previous message is an assistant message
let agentOutput: ChatMessageData | undefined;
if (message.type === "tool_response" && message.result) {
let parsedResult: Record<string, unknown> | null = null;
try {
parsedResult =
typeof message.result === "string"
? JSON.parse(message.result)
: (message.result as Record<string, unknown>);
} catch {
parsedResult = null;
}
if (parsedResult?.type === "agent_output") {
const prevMessage = messages[index - 1];
if (
prevMessage &&
prevMessage.type === "message" &&
prevMessage.role === "assistant"
) {
// This agent output will be rendered inside the previous assistant message
// Skip rendering this message separately
return null;
}
}
}
// Check if next message is an agent_output tool_response to include in current assistant message
if (message.type === "message" && message.role === "assistant") {
const nextMessage = messages[index + 1];
if (
nextMessage &&
nextMessage.type === "tool_response" &&
nextMessage.result
) {
let parsedResult: Record<string, unknown> | null = null;
try {
parsedResult =
typeof nextMessage.result === "string"
? JSON.parse(nextMessage.result)
: (nextMessage.result as Record<string, unknown>);
} catch {
parsedResult = null;
}
if (parsedResult?.type === "agent_output") {
agentOutput = nextMessage;
}
}
}
return (
<ChatMessage
key={index}
message={message}
onSendMessage={onSendMessage}
agentOutput={agentOutput}
/>
);
})}
{/* Render thinking message when streaming but no chunks yet */}
{isStreaming && streamingChunks.length === 0 && <ThinkingMessage />}

View File

@@ -50,11 +50,15 @@ export function ToolResponseMessage({
if (parsedResult && typeof parsedResult === "object") {
const responseType = parsedResult.type as string | undefined;
if (responseType === "agent_output" && parsedResult.execution) {
const execution = parsedResult.execution as {
outputs?: Record<string, unknown[]>;
};
const outputs = execution.outputs || {};
if (responseType === "agent_output") {
const execution = parsedResult.execution as
| {
outputs?: Record<string, unknown[]>;
}
| null
| undefined;
const outputs = execution?.outputs || {};
const message = parsedResult.message as string | undefined;
return (
<div className={cn("space-y-4 px-4 py-2", className)}>
@@ -68,36 +72,45 @@ export function ToolResponseMessage({
{getToolActionPhrase(toolName)}
</Text>
</div>
<div className="space-y-4">
{Object.entries(outputs).map(([outputName, values]) =>
values.map((value, index) => {
const renderer = globalRegistry.getRenderer(value);
if (renderer) {
{message && (
<div className="rounded border p-4">
<Text variant="small" className="text-neutral-600">
{message}
</Text>
</div>
)}
{Object.keys(outputs).length > 0 && (
<div className="space-y-4">
{Object.entries(outputs).map(([outputName, values]) =>
values.map((value, index) => {
const renderer = globalRegistry.getRenderer(value);
if (renderer) {
return (
<OutputItem
key={`${outputName}-${index}`}
value={value}
renderer={renderer}
label={outputName}
/>
);
}
return (
<OutputItem
<div
key={`${outputName}-${index}`}
value={value}
renderer={renderer}
label={outputName}
/>
className="rounded border p-4"
>
<Text variant="large-medium" className="mb-2 capitalize">
{outputName}
</Text>
<pre className="overflow-auto text-sm">
{JSON.stringify(value, null, 2)}
</pre>
</div>
);
}
return (
<div
key={`${outputName}-${index}`}
className="rounded border p-4"
>
<Text variant="large-medium" className="mb-2 capitalize">
{outputName}
</Text>
<pre className="overflow-auto text-sm">
{JSON.stringify(value, null, 2)}
</pre>
</div>
);
}),
)}
</div>
}),
)}
</div>
)}
</div>
);
}
@@ -150,6 +163,67 @@ export function ToolResponseMessage({
</div>
);
}
// Handle other response types with a message field (e.g., understanding_updated)
if (parsedResult.message && typeof parsedResult.message === "string") {
// Format tool name from snake_case to Title Case
const formattedToolName = toolName
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
// Clean up message - remove incomplete user_name references
let cleanedMessage = parsedResult.message;
// Remove "Updated understanding with: user_name" pattern if user_name is just a placeholder
cleanedMessage = cleanedMessage.replace(
/Updated understanding with:\s*user_name\.?\s*/gi,
"",
);
// Remove standalone user_name references
cleanedMessage = cleanedMessage.replace(/\buser_name\b\.?\s*/gi, "");
cleanedMessage = cleanedMessage.trim();
// Only show message if it has content after cleaning
if (!cleanedMessage) {
return (
<div
className={cn(
"flex items-center justify-center gap-2 px-4 py-2",
className,
)}
>
<WrenchIcon
size={14}
weight="bold"
className="flex-shrink-0 text-neutral-500"
/>
<Text variant="small" className="text-neutral-500">
{formattedToolName}
</Text>
</div>
);
}
return (
<div className={cn("space-y-2 px-4 py-2", className)}>
<div className="flex items-center justify-center gap-2">
<WrenchIcon
size={14}
weight="bold"
className="flex-shrink-0 text-neutral-500"
/>
<Text variant="small" className="text-neutral-500">
{formattedToolName}
</Text>
</div>
<div className="rounded border p-4">
<Text variant="small" className="text-neutral-600">
{cleanedMessage}
</Text>
</div>
</div>
);
}
}
const renderer = globalRegistry.getRenderer(result);

View File

@@ -205,7 +205,8 @@ export function RunAgentInputs({
case DataType.MULTI_SELECT: {
const _schema = schema as BlockIOObjectSubSchema;
const allKeys = Object.keys(_schema.properties);
const properties = _schema.properties || {};
const allKeys = Object.keys(properties);
const selectedValues = Object.entries(value || {})
.filter(([_, v]) => v)
.map(([k]) => k);
@@ -214,7 +215,7 @@ export function RunAgentInputs({
<MultiToggle
items={allKeys.map((key) => ({
value: key,
label: _schema.properties[key]?.title ?? key,
label: properties[key]?.title ?? key,
}))}
selectedValues={selectedValues}
onChange={(values: string[]) =>