mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-03 11:24:57 -05:00
Compare commits
21 Commits
fix/file-i
...
abhi/check
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
640b894405 | ||
|
|
ea9f289647 | ||
|
|
d3018cc8ea | ||
|
|
b06868f453 | ||
|
|
7772c71a15 | ||
|
|
8c381faa06 | ||
|
|
d2a1abe3f8 | ||
|
|
15464786c3 | ||
|
|
1b0e1f6e72 | ||
|
|
6730293036 | ||
|
|
432bda5c70 | ||
|
|
e434b59003 | ||
|
|
31ec5f5c17 | ||
|
|
6e0fbdea3c | ||
|
|
b5d6853223 | ||
|
|
afb74a8ff1 | ||
|
|
4c9957dc26 | ||
|
|
26add35418 | ||
|
|
c6e5f83de8 | ||
|
|
c3a126e705 | ||
|
|
73d8323fe4 |
@@ -113,7 +113,7 @@ class StreamToolOutputAvailable(StreamBaseResponse):
|
||||
type: ResponseType = ResponseType.TOOL_OUTPUT_AVAILABLE
|
||||
toolCallId: str = Field(..., description="Tool call ID this responds to")
|
||||
output: str | dict[str, Any] = Field(..., description="Tool execution output")
|
||||
# Additional fields for internal use (not part of AI SDK spec but useful)
|
||||
# Keep these for internal backend use
|
||||
toolName: str | None = Field(
|
||||
default=None, description="Name of the tool that was executed"
|
||||
)
|
||||
@@ -121,6 +121,15 @@ class StreamToolOutputAvailable(StreamBaseResponse):
|
||||
default=True, description="Whether the tool execution succeeded"
|
||||
)
|
||||
|
||||
def to_sse(self) -> str:
|
||||
"""Convert to SSE format, excluding non-spec fields."""
|
||||
import json
|
||||
data = {
|
||||
"type": self.type.value,
|
||||
"toolCallId": self.toolCallId,
|
||||
"output": self.output,
|
||||
}
|
||||
return f"data: {json.dumps(data)}\n\n"
|
||||
|
||||
# ========== Other ==========
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ async def stream_chat_completion(
|
||||
retry_count: int = 0,
|
||||
session: ChatSession | None = None,
|
||||
context: dict[str, str] | None = None, # {url: str, content: str}
|
||||
_continuation_message_id: str | None = None, # Internal: reuse message ID for tool call continuations
|
||||
) -> AsyncGenerator[StreamBaseResponse, None]:
|
||||
"""Main entry point for streaming chat completions with database handling.
|
||||
|
||||
@@ -458,11 +459,15 @@ async def stream_chat_completion(
|
||||
# Generate unique IDs for AI SDK protocol
|
||||
import uuid as uuid_module
|
||||
|
||||
message_id = str(uuid_module.uuid4())
|
||||
# Reuse message ID for continuations (tool call follow-ups) to avoid duplicate messages
|
||||
is_continuation = _continuation_message_id is not None
|
||||
message_id = _continuation_message_id or str(uuid_module.uuid4())
|
||||
text_block_id = str(uuid_module.uuid4())
|
||||
|
||||
# Yield message start
|
||||
yield StreamStart(messageId=message_id)
|
||||
# Only yield message start for the initial call, not for continuations
|
||||
# This prevents the AI SDK from creating duplicate message objects
|
||||
if not is_continuation:
|
||||
yield StreamStart(messageId=message_id)
|
||||
|
||||
try:
|
||||
async for chunk in _stream_chat_chunks(
|
||||
@@ -690,6 +695,7 @@ async def stream_chat_completion(
|
||||
retry_count=retry_count + 1,
|
||||
session=session,
|
||||
context=context,
|
||||
_continuation_message_id=message_id, # Reuse message ID since start was already sent
|
||||
):
|
||||
yield chunk
|
||||
return # Exit after retry to avoid double-saving in finally block
|
||||
@@ -759,6 +765,7 @@ async def stream_chat_completion(
|
||||
session=session, # Pass session object to avoid Redis refetch
|
||||
context=context,
|
||||
tool_call_response=str(tool_response_messages),
|
||||
_continuation_message_id=message_id, # Reuse message ID to avoid duplicates
|
||||
):
|
||||
yield chunk
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "3.0.61",
|
||||
"@faker-js/faker": "10.0.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@next/third-parties": "15.4.6",
|
||||
@@ -60,6 +61,10 @@
|
||||
"@rjsf/utils": "6.1.2",
|
||||
"@rjsf/validator-ajv8": "6.1.2",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@streamdown/cjk": "1.0.1",
|
||||
"@streamdown/code": "1.0.1",
|
||||
"@streamdown/math": "1.0.1",
|
||||
"@streamdown/mermaid": "1.0.1",
|
||||
"@supabase/ssr": "0.7.0",
|
||||
"@supabase/supabase-js": "2.78.0",
|
||||
"@tanstack/react-query": "5.90.6",
|
||||
@@ -68,6 +73,7 @@
|
||||
"@vercel/analytics": "1.5.0",
|
||||
"@vercel/speed-insights": "1.2.0",
|
||||
"@xyflow/react": "12.9.2",
|
||||
"ai": "6.0.59",
|
||||
"boring-avatars": "1.11.2",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -112,9 +118,11 @@
|
||||
"remark-math": "6.0.0",
|
||||
"shepherd.js": "14.5.1",
|
||||
"sonner": "2.0.7",
|
||||
"streamdown": "2.1.0",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwind-scrollbar": "3.1.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"use-stick-to-bottom": "1.1.2",
|
||||
"uuid": "11.1.0",
|
||||
"vaul": "1.1.2",
|
||||
"zod": "3.25.76",
|
||||
|
||||
1161
autogpt_platform/frontend/pnpm-lock.yaml
generated
1161
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
import { UIDataTypes, UITools, UIMessage } from "ai";
|
||||
import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer";
|
||||
import { EmptySession } from "../EmptySession/EmptySession";
|
||||
import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput";
|
||||
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { useState } from "react";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider";
|
||||
|
||||
export interface ChatContainerProps {
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
status: string;
|
||||
error: Error | undefined;
|
||||
input: string;
|
||||
setInput: (input: string) => void;
|
||||
handleMessageSubmit: (e: React.FormEvent) => void;
|
||||
onSend: (message: string) => void;
|
||||
}
|
||||
export const ChatContainer = ({
|
||||
messages,
|
||||
status,
|
||||
error,
|
||||
input,
|
||||
setInput,
|
||||
handleMessageSubmit,
|
||||
onSend,
|
||||
}: ChatContainerProps) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
|
||||
|
||||
async function createSession(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (isCreating) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const response = await postV2CreateSession({
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (response.status === 200 && response.data?.id) {
|
||||
setSessionId(response.data.id);
|
||||
}
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CopilotChatActionsProvider onSend={onSend}>
|
||||
<div className="mx-auto h-full w-full max-w-3xl pb-6">
|
||||
<div className="flex h-full flex-col">
|
||||
{sessionId ? (
|
||||
<ChatMessagesContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
handleSubmit={handleMessageSubmit}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
) : (
|
||||
<EmptySession
|
||||
isCreating={isCreating}
|
||||
onCreateSession={createSession}
|
||||
/>
|
||||
)}
|
||||
<div className="relative px-3 pt-2">
|
||||
<div className="pointer-events-none absolute top-[-18px] z-10 h-6 w-full bg-gradient-to-b from-transparent to-[#f8f8f9]" />
|
||||
<ChatInput
|
||||
onSend={onSend}
|
||||
disabled={status === "streaming" || !sessionId}
|
||||
isStreaming={status === "streaming"}
|
||||
onStop={() => {}}
|
||||
placeholder="You can search or just ask"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotChatActionsProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
Conversation,
|
||||
ConversationContent,
|
||||
ConversationEmptyState,
|
||||
ConversationScrollButton,
|
||||
} from "@/components/ai-elements/conversation";
|
||||
import {
|
||||
Message,
|
||||
MessageContent,
|
||||
MessageResponse,
|
||||
} from "@/components/ai-elements/message";
|
||||
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";
|
||||
import { RunBlockTool } from "../../tools/RunBlock/RunBlock";
|
||||
import { RunAgentTool } from "../../tools/RunAgent/RunAgent";
|
||||
import { ViewAgentOutputTool } from "../../tools/ViewAgentOutput/ViewAgentOutput";
|
||||
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
|
||||
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
|
||||
|
||||
interface ChatMessagesContainerProps {
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
status: string;
|
||||
error: Error | undefined;
|
||||
handleSubmit: (e: React.FormEvent) => void;
|
||||
input: string;
|
||||
setInput: (input: string) => void;
|
||||
}
|
||||
|
||||
export const ChatMessagesContainer = ({
|
||||
messages,
|
||||
status,
|
||||
error,
|
||||
}: ChatMessagesContainerProps) => {
|
||||
return (
|
||||
<Conversation className="flex-1">
|
||||
<ConversationContent>
|
||||
{messages.length === 0 ? (
|
||||
<ConversationEmptyState
|
||||
icon={<MessageSquareIcon className="size-12" />}
|
||||
title="Start a conversation"
|
||||
description="Type a message below to begin chatting"
|
||||
/>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<Message from={message.role} key={message.id}>
|
||||
<MessageContent
|
||||
className={
|
||||
"rounded-xl border px-3 py-2 " +
|
||||
"group-[.is-user]:rounded-2xl group-[.is-user]:border-purple-200 group-[.is-user]:bg-purple-100 group-[.is-user]:text-slate-900 " +
|
||||
"group-[.is-assistant]:border-none group-[.is-assistant]:bg-slate-50/20 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;
|
||||
}
|
||||
})}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
))
|
||||
)}
|
||||
{status === "submitted" && (
|
||||
<Message from="assistant">
|
||||
<MessageContent>
|
||||
<p className="text-zinc-500">Thinking...</p>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
)}
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 p-3 text-red-600">
|
||||
Error: {error.message}
|
||||
</div>
|
||||
)}
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
PlusIcon,
|
||||
SpinnerGapIcon,
|
||||
ChatCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import {
|
||||
postV2CreateSession,
|
||||
useGetV2ListSessions,
|
||||
getGetV2ListSessionsQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export function ChatSidebar() {
|
||||
const { state } = useSidebar();
|
||||
const isCollapsed = state === "collapsed";
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: sessionsResponse, isLoading: isLoadingSessions } =
|
||||
useGetV2ListSessions({ limit: 50 });
|
||||
|
||||
const sessions =
|
||||
sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : [];
|
||||
|
||||
async function handleNewChat() {
|
||||
if (isCreating) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const response = await postV2CreateSession({
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (response.status === 200 && response.data?.id) {
|
||||
setSessionId(response.data.id);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListSessionsQueryKey(),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectSession(id: string) {
|
||||
setSessionId(id);
|
||||
}
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return "Today";
|
||||
if (diffDays === 1) return "Yesterday";
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
variant="inset"
|
||||
collapsible="icon"
|
||||
className="!top-[60px] !h-[calc(100vh-60px)]"
|
||||
>
|
||||
{isCollapsed && (
|
||||
<SidebarHeader
|
||||
className={cn(
|
||||
"flex",
|
||||
isCollapsed
|
||||
? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-start md:justify-start"
|
||||
: "flex-row items-center justify-between",
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
key={isCollapsed ? "header-collapsed" : "header-expanded"}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
isCollapsed ? "flex-row md:flex-col-reverse" : "flex-row",
|
||||
)}
|
||||
initial={{ opacity: 0, filter: "blur(3px)" }}
|
||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||
transition={{ type: "spring", bounce: 0.2 }}
|
||||
>
|
||||
{isCollapsed && (
|
||||
<div className="h-fit rounded-3xl border border-neutral-400 bg-secondary p-1">
|
||||
<SidebarTrigger />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</SidebarHeader>
|
||||
)}
|
||||
<SidebarContent className="gap-4 overflow-y-auto px-2 py-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleNewChat}
|
||||
disabled={isCreating}
|
||||
className={cn(
|
||||
"flex h-fit w-full items-center justify-center gap-2 rounded-3xl border-purple-400 bg-purple-100 px-3 py-2 text-purple-600 hover:border-purple-500 hover:bg-purple-200 hover:text-purple-700",
|
||||
isCollapsed && "justify-center rounded-3xl px-1",
|
||||
)}
|
||||
>
|
||||
{isCreating ? (
|
||||
<SpinnerGapIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
) : (
|
||||
<PlusIcon className="h-4 w-4" weight="bold" />
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<span>{isCreating ? "Creating..." : "New Chat"}</span>
|
||||
)}
|
||||
</Button>
|
||||
{!isCollapsed && (
|
||||
<div className="h-fit rounded-3xl border border-neutral-400 bg-secondary p-1">
|
||||
<SidebarTrigger />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="mt-4 flex flex-col gap-1">
|
||||
{isLoadingSessions ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<SpinnerGapIcon className="h-5 w-5 animate-spin text-neutral-400" />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<p className="py-4 text-center text-sm text-neutral-500">
|
||||
No conversations yet
|
||||
</p>
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800",
|
||||
sessionId === session.id &&
|
||||
"bg-neutral-100 dark:bg-neutral-800",
|
||||
)}
|
||||
>
|
||||
<ChatCircleIcon className="h-4 w-4 shrink-0 text-neutral-500" />
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span className="truncate font-medium">
|
||||
{session.title || `Untitled chat`}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500">
|
||||
{formatDate(session.updated_at)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="px-2"></SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { CopilotChatActionsContext } from "./useCopilotChatActions";
|
||||
|
||||
interface Props {
|
||||
onSend: (message: string) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CopilotChatActionsProvider({ onSend, children }: Props) {
|
||||
return (
|
||||
<CopilotChatActionsContext.Provider value={{ onSend }}>
|
||||
{children}
|
||||
</CopilotChatActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface CopilotChatActions {
|
||||
onSend: (message: string) => void;
|
||||
}
|
||||
|
||||
const CopilotChatActionsContext = createContext<CopilotChatActions | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function useCopilotChatActions(): CopilotChatActions {
|
||||
const ctx = useContext(CopilotChatActionsContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useCopilotChatActions must be used within CopilotChatActionsProvider",
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export { CopilotChatActionsContext };
|
||||
@@ -0,0 +1,23 @@
|
||||
interface Props {
|
||||
isCreating: boolean;
|
||||
onCreateSession: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
export function EmptySession({ isCreating, onCreateSession }: Props) {
|
||||
return (
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center bg-zinc-100 p-4">
|
||||
<h2 className="mb-4 text-xl font-semibold text-zinc-700">
|
||||
Start a new conversation
|
||||
</h2>
|
||||
<form onSubmit={onCreateSession} className="w-full max-w-md">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isCreating}
|
||||
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{isCreating ? "Creating..." : "Start New Chat"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function MorphingTextAnimation({ text }: Props) {
|
||||
const letters = text.split("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<motion.div key={text} className="whitespace-nowrap">
|
||||
<motion.span className="inline-flex overflow-hidden">
|
||||
{letters.map((char, index) => (
|
||||
<motion.span
|
||||
key={`${text}-${index}`}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 8,
|
||||
rotateX: "80deg",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
rotateX: "0deg",
|
||||
filter: "blur(0px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -8,
|
||||
rotateX: "-80deg",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
style={{ willChange: "transform" }}
|
||||
transition={{
|
||||
delay: 0.015 * index,
|
||||
type: "spring",
|
||||
bounce: 0.5,
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
{char === " " ? "\u00A0" : char}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 w-full 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 };
|
||||
}
|
||||
101
autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx
Normal file
101
autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { useState, useMemo } from "react";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar";
|
||||
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
||||
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyIcon, CheckIcon } from "@phosphor-icons/react";
|
||||
|
||||
export default function Page() {
|
||||
const [input, setInput] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [sessionId] = useQueryState("sessionId", parseAsString);
|
||||
|
||||
function handleCopySessionId() {
|
||||
if (!sessionId) return;
|
||||
navigator.clipboard.writeText(sessionId);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
const transport = useMemo(() => {
|
||||
if (!sessionId) return null;
|
||||
return new DefaultChatTransport({
|
||||
api: `/api/chat/sessions/${sessionId}/stream`,
|
||||
prepareSendMessagesRequest: ({ messages }) => {
|
||||
const last = messages[messages.length - 1];
|
||||
return {
|
||||
body: {
|
||||
message: last.parts
|
||||
?.map((p) => (p.type === "text" ? p.text : ""))
|
||||
.join(""),
|
||||
is_user_message: last.role === "user",
|
||||
context: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}, [sessionId]);
|
||||
|
||||
const { messages, sendMessage, status, error } = useChat({
|
||||
id: sessionId ?? undefined,
|
||||
transport: transport ?? undefined,
|
||||
});
|
||||
|
||||
function handleMessageSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || !sessionId) return;
|
||||
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
}
|
||||
|
||||
function onSend(message: string) {
|
||||
sendMessage({ text: message });
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={false}
|
||||
className="h-[calc(100vh-72px)] min-h-0"
|
||||
>
|
||||
<ChatSidebar />
|
||||
<SidebarInset className="relative flex h-[calc(100vh-80px)] flex-col">
|
||||
{sessionId && (
|
||||
<div className="absolute flex items-center px-4 py-4">
|
||||
<div className="flex items-center gap-2 rounded-3xl border border-neutral-400 bg-neutral-100 px-3 py-1.5 text-sm text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400">
|
||||
<span className="text-xs">{sessionId.slice(0, 8)}...</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={handleCopySessionId}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="h-3.5 w-3.5 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
handleMessageSubmit={handleMessageSubmit}
|
||||
onSend={onSend}
|
||||
/>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"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 { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getCreateAgentToolOutput,
|
||||
StateIcon,
|
||||
truncateText,
|
||||
type CreateAgentToolOutput,
|
||||
} from "./helpers";
|
||||
|
||||
export interface CreateAgentToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: CreateAgentToolPart;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: CreateAgentToolOutput): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "agent_saved") {
|
||||
return { badgeText: "Create agent", title: output.agent_name };
|
||||
}
|
||||
if (output.type === "agent_preview") {
|
||||
return {
|
||||
badgeText: "Create agent",
|
||||
title: output.agent_name,
|
||||
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (output.type === "clarification_needed") {
|
||||
return {
|
||||
badgeText: "Create agent",
|
||||
title: "Needs clarification",
|
||||
description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress"
|
||||
) {
|
||||
return { badgeText: "Create agent", title: "Creating agent" };
|
||||
}
|
||||
return { badgeText: "Create agent", title: "Error" };
|
||||
}
|
||||
|
||||
export function CreateAgentTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getCreateAgentToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress" ||
|
||||
output.type === "agent_preview" ||
|
||||
output.type === "agent_saved" ||
|
||||
output.type === "clarification_needed" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleClarificationAnswers(answers: Record<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
.map(([keyword, answer]) => `${keyword}: ${answer}`)
|
||||
.join("\n");
|
||||
|
||||
onSend(
|
||||
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "clarification_needed"}
|
||||
>
|
||||
{(output.type === "operation_started" ||
|
||||
output.type === "operation_pending") && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Operation: {output.operation_id}
|
||||
</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Check your library in a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "operation_in_progress" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Please wait for the current operation to finish.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_saved" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<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"
|
||||
>
|
||||
Open in library
|
||||
</Link>
|
||||
<Link
|
||||
href={output.agent_page_link}
|
||||
className="text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open in builder
|
||||
</Link>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(
|
||||
formatMaybeJson({ agent_id: output.agent_id }),
|
||||
800,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_preview" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{output.description}
|
||||
</p>
|
||||
)}
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(formatMaybeJson(output.agent_json), 1600)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "clarification_needed" && (
|
||||
<ClarificationQuestionsWidget
|
||||
questions={output.questions}
|
||||
message={output.message}
|
||||
onSubmitAnswers={handleClarificationAnswers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface ClarifyingQuestion {
|
||||
question: string;
|
||||
keyword: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface OperationStartedOutput {
|
||||
type: "operation_started";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationPendingOutput {
|
||||
type: "operation_pending";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationInProgressOutput {
|
||||
type: "operation_in_progress";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
tool_call_id: string;
|
||||
}
|
||||
|
||||
export interface AgentPreviewOutput {
|
||||
type: "agent_preview";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_json: Record<string, unknown>;
|
||||
agent_name: string;
|
||||
description: string;
|
||||
node_count: number;
|
||||
link_count: number;
|
||||
}
|
||||
|
||||
export interface AgentSavedOutput {
|
||||
type: "agent_saved";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
library_agent_id: string;
|
||||
library_agent_link: string;
|
||||
agent_page_link: string;
|
||||
}
|
||||
|
||||
export interface ClarificationNeededOutput {
|
||||
type: "clarification_needed";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
questions: ClarifyingQuestion[];
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type CreateAgentToolOutput =
|
||||
| OperationStartedOutput
|
||||
| OperationPendingOutput
|
||||
| OperationInProgressOutput
|
||||
| AgentPreviewOutput
|
||||
| AgentSavedOutput
|
||||
| ClarificationNeededOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): CreateAgentToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as CreateAgentToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as CreateAgentToolOutput;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getCreateAgentToolOutput(
|
||||
part: unknown,
|
||||
): CreateAgentToolOutput | null {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return parseOutput((part as { output?: unknown }).output);
|
||||
}
|
||||
|
||||
export function getAnimationText(part: {
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Creating agent";
|
||||
case "input-available":
|
||||
return "Generating agent workflow";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Agent created";
|
||||
if (output.type === "operation_started") return "Agent creation started";
|
||||
if (output.type === "operation_pending")
|
||||
return "Agent creation in progress";
|
||||
if (output.type === "operation_in_progress")
|
||||
return "Agent creation already in progress";
|
||||
if (output.type === "agent_saved") return `Saved: ${output.agent_name}`;
|
||||
if (output.type === "agent_preview")
|
||||
return `Preview: ${output.agent_name}`;
|
||||
if (output.type === "clarification_needed") return "Needs clarification";
|
||||
return "Error creating agent";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error creating agent";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMaybeJson(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function truncateText(text: string, maxChars: number): string {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length <= maxChars) return trimmed;
|
||||
return `${trimmed.slice(0, maxChars).trimEnd()}…`;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"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 { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getEditAgentToolOutput,
|
||||
StateIcon,
|
||||
truncateText,
|
||||
type EditAgentToolOutput,
|
||||
} from "./helpers";
|
||||
|
||||
export interface EditAgentToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: EditAgentToolPart;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: EditAgentToolOutput): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "agent_saved") {
|
||||
return { badgeText: "Edit agent", title: output.agent_name };
|
||||
}
|
||||
if (output.type === "agent_preview") {
|
||||
return {
|
||||
badgeText: "Edit agent",
|
||||
title: output.agent_name,
|
||||
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (output.type === "clarification_needed") {
|
||||
return {
|
||||
badgeText: "Edit agent",
|
||||
title: "Needs clarification",
|
||||
description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress"
|
||||
) {
|
||||
return { badgeText: "Edit agent", title: "Editing agent" };
|
||||
}
|
||||
return { badgeText: "Edit agent", title: "Error" };
|
||||
}
|
||||
|
||||
export function EditAgentTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getEditAgentToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress" ||
|
||||
output.type === "agent_preview" ||
|
||||
output.type === "agent_saved" ||
|
||||
output.type === "clarification_needed" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleClarificationAnswers(answers: Record<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
.map(([keyword, answer]) => `${keyword}: ${answer}`)
|
||||
.join("\n");
|
||||
|
||||
onSend(
|
||||
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with editing the agent.`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "clarification_needed"}
|
||||
>
|
||||
{(output.type === "operation_started" ||
|
||||
output.type === "operation_pending") && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Operation: {output.operation_id}
|
||||
</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Check your library in a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "operation_in_progress" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Please wait for the current operation to finish.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_saved" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<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"
|
||||
>
|
||||
Open in library
|
||||
</Link>
|
||||
<Link
|
||||
href={output.agent_page_link}
|
||||
className="text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open in builder
|
||||
</Link>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(
|
||||
formatMaybeJson({ agent_id: output.agent_id }),
|
||||
800,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_preview" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{output.description}
|
||||
</p>
|
||||
)}
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(formatMaybeJson(output.agent_json), 1600)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "clarification_needed" && (
|
||||
<ClarificationQuestionsWidget
|
||||
questions={output.questions}
|
||||
message={output.message}
|
||||
onSubmitAnswers={handleClarificationAnswers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface ClarifyingQuestion {
|
||||
question: string;
|
||||
keyword: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface OperationStartedOutput {
|
||||
type: "operation_started";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationPendingOutput {
|
||||
type: "operation_pending";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationInProgressOutput {
|
||||
type: "operation_in_progress";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
tool_call_id: string;
|
||||
}
|
||||
|
||||
export interface AgentPreviewOutput {
|
||||
type: "agent_preview";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_json: Record<string, unknown>;
|
||||
agent_name: string;
|
||||
description: string;
|
||||
node_count: number;
|
||||
link_count: number;
|
||||
}
|
||||
|
||||
export interface AgentSavedOutput {
|
||||
type: "agent_saved";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
library_agent_id: string;
|
||||
library_agent_link: string;
|
||||
agent_page_link: string;
|
||||
}
|
||||
|
||||
export interface ClarificationNeededOutput {
|
||||
type: "clarification_needed";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
questions: ClarifyingQuestion[];
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type EditAgentToolOutput =
|
||||
| OperationStartedOutput
|
||||
| OperationPendingOutput
|
||||
| OperationInProgressOutput
|
||||
| AgentPreviewOutput
|
||||
| AgentSavedOutput
|
||||
| ClarificationNeededOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): EditAgentToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as EditAgentToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as EditAgentToolOutput;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getEditAgentToolOutput(
|
||||
part: unknown,
|
||||
): EditAgentToolOutput | null {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return parseOutput((part as { output?: unknown }).output);
|
||||
}
|
||||
|
||||
export function getAnimationText(part: {
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Editing agent";
|
||||
case "input-available":
|
||||
return "Updating agent workflow";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Agent updated";
|
||||
if (output.type === "operation_started") return "Agent update started";
|
||||
if (output.type === "operation_pending")
|
||||
return "Agent update in progress";
|
||||
if (output.type === "operation_in_progress")
|
||||
return "Agent update already in progress";
|
||||
if (output.type === "agent_saved") return `Saved: ${output.agent_name}`;
|
||||
if (output.type === "agent_preview")
|
||||
return `Preview: ${output.agent_name}`;
|
||||
if (output.type === "clarification_needed") return "Needs clarification";
|
||||
return "Error editing agent";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error editing agent";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMaybeJson(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function truncateText(text: string, maxChars: number): string {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length <= maxChars) return trimmed;
|
||||
return `${trimmed.slice(0, maxChars).trimEnd()}…`;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { ToolUIPart } from "ai";
|
||||
import Link from "next/link";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import {
|
||||
getAgentHref,
|
||||
getAnimationText,
|
||||
getFindAgentsOutput,
|
||||
getSourceLabelFromToolType,
|
||||
StateIcon,
|
||||
} from "./helpers";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
|
||||
export interface FindAgentsToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: FindAgentsToolPart;
|
||||
}
|
||||
|
||||
export function FindAgentsTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const output = getFindAgentsOutput(part);
|
||||
|
||||
const query =
|
||||
typeof part.input === "object" && part.input !== null
|
||||
? String((part.input as { query?: unknown }).query ?? "").trim()
|
||||
: "";
|
||||
|
||||
const isAgentsFound =
|
||||
part.state === "output-available" && output?.type === "agents_found";
|
||||
const hasAgents =
|
||||
isAgentsFound &&
|
||||
output.agents.length > 0 &&
|
||||
(typeof output.count !== "number" || output.count > 0);
|
||||
const totalCount = isAgentsFound ? output.count : 0;
|
||||
const { label: sourceLabel, source } = getSourceLabelFromToolType(part.type);
|
||||
const scopeText =
|
||||
source === "library"
|
||||
? "in your library"
|
||||
: source === "marketplace"
|
||||
? "in marketplace"
|
||||
: "";
|
||||
const accordionDescription = `Found ${totalCount}${scopeText ? ` ${scopeText}` : ""}${
|
||||
query ? ` for "${query}"` : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasAgents && (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface FindAgentInput {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface AgentInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
source?: "marketplace" | "library" | string;
|
||||
}
|
||||
|
||||
export interface AgentsFoundOutput {
|
||||
type: "agents_found";
|
||||
title?: string;
|
||||
message?: string;
|
||||
session_id?: string;
|
||||
agents: AgentInfo[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface NoResultsOutput {
|
||||
type: "no_results";
|
||||
message: string;
|
||||
suggestions?: string[];
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
error?: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export type FindAgentsOutput =
|
||||
| AgentsFoundOutput
|
||||
| NoResultsOutput
|
||||
| ErrorOutput;
|
||||
|
||||
export type FindAgentsToolType =
|
||||
| "tool-find_agent"
|
||||
| "tool-find_library_agent"
|
||||
| (string & {});
|
||||
|
||||
function parseOutput(output: unknown): FindAgentsOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as FindAgentsOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") {
|
||||
return output as FindAgentsOutput;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getFindAgentsOutput(part: unknown): FindAgentsOutput | null {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return parseOutput((part as { output?: unknown }).output);
|
||||
}
|
||||
|
||||
export function getSourceLabelFromToolType(toolType?: FindAgentsToolType): {
|
||||
source: "marketplace" | "library" | "unknown";
|
||||
label: string;
|
||||
} {
|
||||
if (toolType === "tool-find_library_agent") {
|
||||
return { source: "library", label: "Library" };
|
||||
}
|
||||
if (toolType === "tool-find_agent") {
|
||||
return { source: "marketplace", label: "Marketplace" };
|
||||
}
|
||||
return { source: "unknown", label: "Agents" };
|
||||
}
|
||||
|
||||
export function getAnimationText(part: {
|
||||
type?: FindAgentsToolType;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
const { label, source } = getSourceLabelFromToolType(part.type);
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return `Searching ${label.toLowerCase()} agents for you`;
|
||||
|
||||
case "input-available": {
|
||||
const query = (part.input as FindAgentInput | undefined)?.query?.trim();
|
||||
if (query) {
|
||||
return source === "library"
|
||||
? `Finding library agents matching "${query}"`
|
||||
: `Finding marketplace agents matching "${query}"`;
|
||||
}
|
||||
return source === "library" ? "Finding library agents" : "Finding agents";
|
||||
}
|
||||
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
const query = (part.input as FindAgentInput | undefined)?.query?.trim();
|
||||
const scope = source === "library" ? "in your library" : "in marketplace";
|
||||
if (!output) {
|
||||
return query ? `Found agents ${scope} for "${query}"` : "Found agents";
|
||||
}
|
||||
if (output.type === "no_results") {
|
||||
return query
|
||||
? `No agents found ${scope} for "${query}"`
|
||||
: `No agents found ${scope}`;
|
||||
}
|
||||
if (output.type === "agents_found") {
|
||||
const count = output.count ?? output.agents?.length ?? 0;
|
||||
const countText = `Found ${count} agent${count === 1 ? "" : "s"}`;
|
||||
if (query) return `${countText} ${scope} for "${query}"`;
|
||||
return `${countText} ${scope}`;
|
||||
}
|
||||
if (output.type === "error") {
|
||||
return `Error finding agents ${scope}`;
|
||||
}
|
||||
return `Found agents ${scope}`;
|
||||
}
|
||||
|
||||
case "output-error":
|
||||
return source === "library"
|
||||
? "Error finding agents in your library"
|
||||
: "Error finding agents in marketplace";
|
||||
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgentHref(agent: AgentInfo): string | null {
|
||||
if (agent.source === "library") {
|
||||
return `/library/agents/${encodeURIComponent(agent.id)}`;
|
||||
}
|
||||
|
||||
const [creator, slug, ...rest] = agent.id.split("/");
|
||||
if (!creator || !slug || rest.length > 0) return null;
|
||||
return `/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`;
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
|
||||
import { ToolUIPart } from "ai";
|
||||
import { getAnimationText, StateIcon } from "./helpers";
|
||||
|
||||
export interface FindBlockInput {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface FindBlockOutput {
|
||||
type: "block_list";
|
||||
message: string;
|
||||
session_id: string;
|
||||
blocks: BlockInfo[];
|
||||
count: number;
|
||||
query: string;
|
||||
usage_hint: string;
|
||||
}
|
||||
|
||||
export interface FindBlockToolPart {
|
||||
type: string;
|
||||
toolName?: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: FindBlockInput | unknown;
|
||||
output?: string | FindBlockOutput | unknown;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: FindBlockToolPart;
|
||||
}
|
||||
|
||||
export function FindBlocksTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ToolUIPart } from "ai";
|
||||
import {
|
||||
FindBlockInput,
|
||||
FindBlockOutput,
|
||||
FindBlockToolPart,
|
||||
} from "./FindBlocks";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export const getAnimationText = (part: FindBlockToolPart): string => {
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Searching blocks for you";
|
||||
|
||||
case "input-available": {
|
||||
const query = (part.input as FindBlockInput).query;
|
||||
return `Finding "${query}" blocks`;
|
||||
}
|
||||
|
||||
case "output-available": {
|
||||
const parsed = JSON.parse(part.output as string) as FindBlockOutput;
|
||||
if (parsed) {
|
||||
return `Found ${parsed.count} "${(part.input as FindBlockInput).query}" blocks`;
|
||||
}
|
||||
return "Found blocks";
|
||||
}
|
||||
|
||||
case "output-error":
|
||||
return "Error finding blocks";
|
||||
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
};
|
||||
|
||||
export const StateIcon = ({ state }: { state: ToolUIPart["state"] }) => {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
"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 { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getRunAgentToolOutput,
|
||||
StateIcon,
|
||||
type RunAgentToolOutput,
|
||||
} from "./helpers";
|
||||
|
||||
export interface RunAgentToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: RunAgentToolPart;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: RunAgentToolOutput): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "execution_started") {
|
||||
return {
|
||||
badgeText: "Run agent",
|
||||
title: output.graph_name,
|
||||
description: `Status: ${output.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (output.type === "agent_details") {
|
||||
return {
|
||||
badgeText: "Run agent",
|
||||
title: output.agent.name,
|
||||
description: "Inputs required",
|
||||
};
|
||||
}
|
||||
|
||||
if (output.type === "setup_requirements") {
|
||||
const missingCredsCount = Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).length;
|
||||
return {
|
||||
badgeText: "Run agent",
|
||||
title: output.setup_info.agent_name,
|
||||
description:
|
||||
missingCredsCount > 0
|
||||
? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}`
|
||||
: output.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (output.type === "need_login") {
|
||||
return { badgeText: "Run agent", title: "Sign in required" };
|
||||
}
|
||||
|
||||
return { badgeText: "Run agent", title: "Error" };
|
||||
}
|
||||
|
||||
export function RunAgentTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getRunAgentToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "execution_started" ||
|
||||
output.type === "agent_details" ||
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "need_login" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleAllCredentialsComplete() {
|
||||
onSend(
|
||||
"I've configured the required credentials. Please check if everything is ready and proceed with running the agent.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "agent_details"
|
||||
}
|
||||
>
|
||||
{output.type === "execution_started" && (
|
||||
<div className="grid gap-2">
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<div className="flex items-start justify-between gap-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>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_details" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{output.agent.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{output.agent.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">Inputs</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Provide required inputs in chat, or ask to run with defaults.
|
||||
</p>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.agent.inputs)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "setup_requirements" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).length > 0 && (
|
||||
<ChatCredentialsSetup
|
||||
credentials={Object.values(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).map((cred) => ({
|
||||
provider: cred.provider,
|
||||
providerName:
|
||||
cred.provider_name ?? cred.provider.replace(/_/g, " "),
|
||||
credentialTypes: (cred.types ?? [cred.type]) as Array<
|
||||
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
||||
>,
|
||||
title: cred.title,
|
||||
scopes: cred.scopes,
|
||||
}))}
|
||||
agentName={output.setup_info.agent_name}
|
||||
message={output.message}
|
||||
onAllCredentialsComplete={handleAllCredentialsComplete}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.setup_info.requirements.inputs?.length > 0 && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Expected inputs
|
||||
</p>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{output.setup_info.requirements.inputs.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">
|
||||
{input.title}
|
||||
</p>
|
||||
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||
{input.required ? "Required" : "Optional"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{input.name} • {input.type}
|
||||
{input.description ? ` • ${input.description}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "need_login" && (
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface RunAgentInput {
|
||||
username_agent_slug?: string;
|
||||
library_agent_id?: string;
|
||||
inputs?: Record<string, unknown>;
|
||||
use_defaults?: boolean;
|
||||
schedule_name?: string;
|
||||
cron?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export interface CredentialsMeta {
|
||||
id: string;
|
||||
provider: string;
|
||||
provider_name?: string;
|
||||
type: string;
|
||||
types?: string[];
|
||||
title: string;
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
export interface SetupInfo {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
requirements: {
|
||||
credentials: CredentialsMeta[];
|
||||
inputs: Array<{
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
}>;
|
||||
execution_modes: string[];
|
||||
};
|
||||
user_readiness: {
|
||||
has_all_credentials: boolean;
|
||||
missing_credentials: Record<string, CredentialsMeta>;
|
||||
ready_to_run: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetupRequirementsOutput {
|
||||
type: "setup_requirements";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
setup_info: SetupInfo;
|
||||
graph_id?: string | null;
|
||||
graph_version?: number | null;
|
||||
}
|
||||
|
||||
export interface ExecutionStartedOutput {
|
||||
type: "execution_started";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
execution_id: string;
|
||||
graph_id: string;
|
||||
graph_name: string;
|
||||
library_agent_id?: string | null;
|
||||
library_agent_link?: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface NeedLoginOutput {
|
||||
type: "need_login";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export interface AgentDetailsOutput {
|
||||
type: "agent_details";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
inputs: Record<string, unknown>;
|
||||
credentials: CredentialsMeta[];
|
||||
execution_options?: {
|
||||
manual?: boolean;
|
||||
scheduled?: boolean;
|
||||
webhook?: boolean;
|
||||
};
|
||||
};
|
||||
user_authenticated?: boolean;
|
||||
graph_id?: string | null;
|
||||
graph_version?: number | null;
|
||||
}
|
||||
|
||||
export type RunAgentToolOutput =
|
||||
| SetupRequirementsOutput
|
||||
| ExecutionStartedOutput
|
||||
| AgentDetailsOutput
|
||||
| NeedLoginOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): RunAgentToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as RunAgentToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as RunAgentToolOutput;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getRunAgentToolOutput(
|
||||
part: unknown,
|
||||
): RunAgentToolOutput | null {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return parseOutput((part as { output?: unknown }).output);
|
||||
}
|
||||
|
||||
function getAgentIdentifierText(
|
||||
input: RunAgentInput | undefined,
|
||||
): string | null {
|
||||
if (!input) return null;
|
||||
const slug = input.username_agent_slug?.trim();
|
||||
if (slug) return slug;
|
||||
const libraryId = input.library_agent_id?.trim();
|
||||
if (libraryId) return `Library agent ${libraryId}`;
|
||||
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;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
const input = part.input as RunAgentInput | undefined;
|
||||
const agentIdentifier = getAgentIdentifierText(input);
|
||||
const mode = getExecutionModeText(input);
|
||||
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Preparing to run agent";
|
||||
case "input-available":
|
||||
return agentIdentifier ? `${mode}: ${agentIdentifier}` : "Running agent";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Agent run updated";
|
||||
if (output.type === "execution_started") {
|
||||
return `Started: ${output.graph_name}`;
|
||||
}
|
||||
if (output.type === "agent_details") {
|
||||
return `Agent inputs: ${output.agent.name}`;
|
||||
}
|
||||
if (output.type === "setup_requirements") {
|
||||
return `Needs setup: ${output.setup_info.agent_name}`;
|
||||
}
|
||||
if (output.type === "need_login") return "Sign in required to run agent";
|
||||
return "Error running agent";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error running agent";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMaybeJson(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getRunBlockToolOutput,
|
||||
StateIcon,
|
||||
type RunBlockToolOutput,
|
||||
} from "./helpers";
|
||||
|
||||
export interface RunBlockToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: RunBlockToolPart;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: RunBlockToolOutput): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "block_output") {
|
||||
const keys = Object.keys(output.outputs ?? {});
|
||||
return {
|
||||
badgeText: "Run block",
|
||||
title: output.block_name,
|
||||
description:
|
||||
keys.length > 0
|
||||
? `${keys.length} output key${keys.length === 1 ? "" : "s"}`
|
||||
: output.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (output.type === "setup_requirements") {
|
||||
const missingCredsCount = Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).length;
|
||||
return {
|
||||
badgeText: "Run block",
|
||||
title: output.setup_info.agent_name,
|
||||
description:
|
||||
missingCredsCount > 0
|
||||
? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}`
|
||||
: output.message,
|
||||
};
|
||||
}
|
||||
|
||||
return { badgeText: "Run block", title: "Error" };
|
||||
}
|
||||
|
||||
export function RunBlockTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getRunBlockToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "block_output" ||
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleAllCredentialsComplete() {
|
||||
onSend(
|
||||
"I've configured the required credentials. Please re-run the block now.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "setup_requirements"}
|
||||
>
|
||||
{output.type === "block_output" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{Object.entries(output.outputs ?? {}).map(([key, items]) => (
|
||||
<div key={key} className="rounded-2xl border bg-background p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-xs font-medium text-foreground">
|
||||
{key}
|
||||
</p>
|
||||
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||
{items.length} item{items.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(items.slice(0, 3))}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "setup_requirements" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).length > 0 && (
|
||||
<ChatCredentialsSetup
|
||||
credentials={Object.values(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).map((cred) => ({
|
||||
provider: cred.provider,
|
||||
providerName:
|
||||
cred.provider_name ?? cred.provider.replace(/_/g, " "),
|
||||
credentialTypes: (cred.types ?? [cred.type]) as Array<
|
||||
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
||||
>,
|
||||
title: cred.title,
|
||||
scopes: cred.scopes,
|
||||
}))}
|
||||
agentName={output.setup_info.agent_name}
|
||||
message={output.message}
|
||||
onAllCredentialsComplete={handleAllCredentialsComplete}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.setup_info.requirements.inputs?.length > 0 && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Expected inputs
|
||||
</p>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{output.setup_info.requirements.inputs.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">
|
||||
{input.title}
|
||||
</p>
|
||||
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||
{input.required ? "Required" : "Optional"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{input.name} • {input.type}
|
||||
{input.description ? ` • ${input.description}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface RunBlockInput {
|
||||
block_id?: string;
|
||||
input_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CredentialsMeta {
|
||||
id: string;
|
||||
provider: string;
|
||||
provider_name?: string;
|
||||
type: string;
|
||||
types?: string[];
|
||||
title: string;
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
export interface SetupInfo {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
requirements: {
|
||||
credentials: CredentialsMeta[];
|
||||
inputs: Array<{
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
}>;
|
||||
execution_modes: string[];
|
||||
};
|
||||
user_readiness: {
|
||||
has_all_credentials: boolean;
|
||||
missing_credentials: Record<string, CredentialsMeta>;
|
||||
ready_to_run: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetupRequirementsOutput {
|
||||
type: "setup_requirements";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
setup_info: SetupInfo;
|
||||
}
|
||||
|
||||
export interface BlockOutput {
|
||||
type: "block_output";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
block_id: string;
|
||||
block_name: string;
|
||||
outputs: Record<string, unknown[]>;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type RunBlockToolOutput =
|
||||
| SetupRequirementsOutput
|
||||
| BlockOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): RunBlockToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as RunBlockToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as RunBlockToolOutput;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getRunBlockToolOutput(
|
||||
part: unknown,
|
||||
): RunBlockToolOutput | null {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return parseOutput((part as { output?: unknown }).output);
|
||||
}
|
||||
|
||||
function getBlockLabel(input: RunBlockInput | undefined): string | null {
|
||||
const blockId = input?.block_id?.trim();
|
||||
if (!blockId) return null;
|
||||
return `Block ${blockId.slice(0, 8)}…`;
|
||||
}
|
||||
|
||||
export function getAnimationText(part: {
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
const input = part.input as RunBlockInput | undefined;
|
||||
const blockLabel = getBlockLabel(input);
|
||||
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Preparing to run block";
|
||||
case "input-available":
|
||||
return blockLabel ? `Running ${blockLabel}` : "Running block";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Block run updated";
|
||||
if (output.type === "block_output")
|
||||
return `Block ran: ${output.block_name}`;
|
||||
if (output.type === "setup_requirements") {
|
||||
return `Needs setup: ${output.setup_info.agent_name}`;
|
||||
}
|
||||
return "Error running block";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error running block";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMaybeJson(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import type { ToolUIPart } from "ai";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
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 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");
|
||||
|
||||
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">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && normalized && (
|
||||
<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">
|
||||
{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>
|
||||
<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 === "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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function toDocsUrl(path: string): string {
|
||||
const urlPath = path.includes(".")
|
||||
? path.slice(0, path.lastIndexOf("."))
|
||||
: path;
|
||||
return `https://docs.agpt.co/${urlPath}`;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
"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 {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getViewAgentOutputToolOutput,
|
||||
StateIcon,
|
||||
type ViewAgentOutputToolOutput,
|
||||
} from "./helpers";
|
||||
|
||||
export interface ViewAgentOutputToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: ViewAgentOutputToolPart;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: ViewAgentOutputToolOutput): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "agent_output") {
|
||||
const status = output.execution?.status;
|
||||
return {
|
||||
badgeText: "Agent output",
|
||||
title: output.agent_name,
|
||||
description: status ? `Status: ${status}` : output.message,
|
||||
};
|
||||
}
|
||||
if (output.type === "no_results") {
|
||||
return { badgeText: "Agent output", title: "No results" };
|
||||
}
|
||||
return { badgeText: "Agent output", title: "Error" };
|
||||
}
|
||||
|
||||
export function ViewAgentOutputTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
|
||||
const output = getViewAgentOutputToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "agent_output" ||
|
||||
output.type === "no_results" ||
|
||||
output.type === "error");
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion {...getAccordionMeta(output)}>
|
||||
{output.type === "agent_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>
|
||||
|
||||
{output.execution ? (
|
||||
<div className="grid gap-2">
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Execution
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{output.execution.execution_id}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Status: {output.execution.status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{output.execution.inputs_summary && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Inputs summary
|
||||
</p>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.execution.inputs_summary)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.entries(output.execution.outputs ?? {}).map(
|
||||
([key, items]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="rounded-2xl border bg-background p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-xs font-medium text-foreground">
|
||||
{key}
|
||||
</p>
|
||||
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||
{items.length} item{items.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(items.slice(0, 3))}
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
Try asking for a specific run or execution_id.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "no_results" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface ViewAgentOutputInput {
|
||||
agent_name?: string;
|
||||
library_agent_id?: string;
|
||||
store_slug?: string;
|
||||
execution_id?: string;
|
||||
run_time?: string;
|
||||
}
|
||||
|
||||
export interface ExecutionOutputInfo {
|
||||
execution_id: string;
|
||||
status: string;
|
||||
started_at?: string | null;
|
||||
ended_at?: string | null;
|
||||
outputs: Record<string, unknown[]>;
|
||||
inputs_summary?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AgentOutputOutput {
|
||||
type: "agent_output";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_name: string;
|
||||
agent_id: string;
|
||||
library_agent_id?: string | null;
|
||||
library_agent_link?: string | null;
|
||||
execution?: ExecutionOutputInfo | null;
|
||||
available_executions?: Array<Record<string, unknown>> | null;
|
||||
total_executions: number;
|
||||
}
|
||||
|
||||
export interface NoResultsOutput {
|
||||
type: "no_results";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type ViewAgentOutputToolOutput =
|
||||
| AgentOutputOutput
|
||||
| NoResultsOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): ViewAgentOutputToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as ViewAgentOutputToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as ViewAgentOutputToolOutput;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getViewAgentOutputToolOutput(
|
||||
part: unknown,
|
||||
): ViewAgentOutputToolOutput | null {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return parseOutput((part as { output?: unknown }).output);
|
||||
}
|
||||
|
||||
function getAgentIdentifierText(
|
||||
input: ViewAgentOutputInput | undefined,
|
||||
): string | null {
|
||||
if (!input) return null;
|
||||
const libraryId = input.library_agent_id?.trim();
|
||||
if (libraryId) return `Library agent ${libraryId}`;
|
||||
const slug = input.store_slug?.trim();
|
||||
if (slug) return slug;
|
||||
const name = input.agent_name?.trim();
|
||||
if (name) return name;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAnimationText(part: {
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
const input = part.input as ViewAgentOutputInput | undefined;
|
||||
const agent = getAgentIdentifierText(input);
|
||||
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Looking up agent outputs";
|
||||
case "input-available":
|
||||
return agent ? `Loading outputs: ${agent}` : "Loading agent outputs";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Loaded agent outputs";
|
||||
if (output.type === "agent_output") {
|
||||
if (output.execution)
|
||||
return `Loaded output (${output.execution.status})`;
|
||||
return "Loaded agent outputs";
|
||||
}
|
||||
if (output.type === "no_results") return "No outputs found";
|
||||
return "Error loading agent output";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error loading agent output";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMaybeJson(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@source "../node_modules/streamdown/dist/*.js";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@@ -29,6 +30,14 @@
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -56,6 +65,14 @@
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowDownIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||
|
||||
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||
|
||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
<StickToBottom
|
||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
||||
initial="smooth"
|
||||
resize="smooth"
|
||||
role="log"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ConversationContentProps = ComponentProps<
|
||||
typeof StickToBottom.Content
|
||||
>;
|
||||
|
||||
export const ConversationContent = ({
|
||||
className,
|
||||
...props
|
||||
}: ConversationContentProps) => (
|
||||
<StickToBottom.Content
|
||||
className={cn("flex flex-col gap-8 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ConversationEmptyState = ({
|
||||
className,
|
||||
title = "No messages yet",
|
||||
description = "Start a conversation to see messages here",
|
||||
icon,
|
||||
children,
|
||||
...props
|
||||
}: ConversationEmptyStateProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
{icon && (
|
||||
<div className="text-neutral-500 dark:text-neutral-400">{icon}</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const ConversationScrollButton = ({
|
||||
className,
|
||||
...props
|
||||
}: ConversationScrollButtonProps) => {
|
||||
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
||||
|
||||
const handleScrollToBottom = useCallback(() => {
|
||||
scrollToBottom();
|
||||
}, [scrollToBottom]);
|
||||
|
||||
return (
|
||||
!isAtBottom && (
|
||||
<Button
|
||||
className={cn(
|
||||
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-white dark:dark:bg-neutral-950 dark:dark:hover:bg-neutral-800 dark:hover:bg-neutral-100",
|
||||
className,
|
||||
)}
|
||||
onClick={handleScrollToBottom}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
{...props}
|
||||
>
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
};
|
||||
338
autogpt_platform/frontend/src/components/ai-elements/message.tsx
Normal file
338
autogpt_platform/frontend/src/components/ai-elements/message.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cjk } from "@streamdown/cjk";
|
||||
import { code } from "@streamdown/code";
|
||||
import { math } from "@streamdown/math";
|
||||
import { mermaid } from "@streamdown/mermaid";
|
||||
import type { UIMessage } from "ai";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage["role"];
|
||||
};
|
||||
|
||||
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex w-full max-w-[95%] flex-col gap-2",
|
||||
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const MessageContent = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: MessageContentProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"is-user:dark flex w-full min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
|
||||
"group-[.is-user]:w-fit",
|
||||
"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-neutral-100 group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-neutral-950 dark:group-[.is-user]:bg-neutral-800 dark:group-[.is-user]:text-neutral-50",
|
||||
"group-[.is-assistant]:text-neutral-950 dark:group-[.is-assistant]:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type MessageActionsProps = ComponentProps<"div">;
|
||||
|
||||
export const MessageActions = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MessageActionsProps) => (
|
||||
<div className={cn("flex items-center gap-1", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type MessageActionProps = ComponentProps<typeof Button> & {
|
||||
tooltip?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const MessageAction = ({
|
||||
tooltip,
|
||||
children,
|
||||
label,
|
||||
variant = "ghost",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: MessageActionProps) => {
|
||||
const button = (
|
||||
<Button size={size} type="button" variant={variant} {...props}>
|
||||
{children}
|
||||
<span className="sr-only">{label || tooltip}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
interface MessageBranchContextType {
|
||||
currentBranch: number;
|
||||
totalBranches: number;
|
||||
goToPrevious: () => void;
|
||||
goToNext: () => void;
|
||||
branches: ReactElement[];
|
||||
setBranches: (branches: ReactElement[]) => void;
|
||||
}
|
||||
|
||||
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const useMessageBranch = () => {
|
||||
const context = useContext(MessageBranchContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("MessageBranch components must be used within");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
|
||||
defaultBranch?: number;
|
||||
onBranchChange?: (branchIndex: number) => void;
|
||||
};
|
||||
|
||||
export const MessageBranch = ({
|
||||
defaultBranch = 0,
|
||||
onBranchChange,
|
||||
className,
|
||||
...props
|
||||
}: MessageBranchProps) => {
|
||||
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
||||
const [branches, setBranches] = useState<ReactElement[]>([]);
|
||||
|
||||
const handleBranchChange = (newBranch: number) => {
|
||||
setCurrentBranch(newBranch);
|
||||
onBranchChange?.(newBranch);
|
||||
};
|
||||
|
||||
const goToPrevious = () => {
|
||||
const newBranch =
|
||||
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
||||
handleBranchChange(newBranch);
|
||||
};
|
||||
|
||||
const goToNext = () => {
|
||||
const newBranch =
|
||||
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
||||
handleBranchChange(newBranch);
|
||||
};
|
||||
|
||||
const contextValue: MessageBranchContextType = {
|
||||
currentBranch,
|
||||
totalBranches: branches.length,
|
||||
goToPrevious,
|
||||
goToNext,
|
||||
branches,
|
||||
setBranches,
|
||||
};
|
||||
|
||||
return (
|
||||
<MessageBranchContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
</MessageBranchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const MessageBranchContent = ({
|
||||
children,
|
||||
...props
|
||||
}: MessageBranchContentProps) => {
|
||||
const { currentBranch, setBranches, branches } = useMessageBranch();
|
||||
const childrenArray = Array.isArray(children) ? children : [children];
|
||||
|
||||
// Use useEffect to update branches when they change
|
||||
useEffect(() => {
|
||||
if (branches.length !== childrenArray.length) {
|
||||
setBranches(childrenArray);
|
||||
}
|
||||
}, [childrenArray, branches, setBranches]);
|
||||
|
||||
return childrenArray.map((branch, index) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
||||
index === currentBranch ? "block" : "hidden",
|
||||
)}
|
||||
key={branch.key}
|
||||
{...props}
|
||||
>
|
||||
{branch}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage["role"];
|
||||
};
|
||||
|
||||
export const MessageBranchSelector = ({
|
||||
className,
|
||||
from: _from,
|
||||
...props
|
||||
}: MessageBranchSelectorProps) => {
|
||||
const { totalBranches } = useMessageBranch();
|
||||
|
||||
// Don't render if there's only one branch
|
||||
if (totalBranches <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup
|
||||
className={cn(
|
||||
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
|
||||
className,
|
||||
)}
|
||||
orientation="horizontal"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const MessageBranchPrevious = ({
|
||||
children,
|
||||
...props
|
||||
}: MessageBranchPreviousProps) => {
|
||||
const { goToPrevious, totalBranches } = useMessageBranch();
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Previous branch"
|
||||
disabled={totalBranches <= 1}
|
||||
onClick={goToPrevious}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronLeftIcon size={14} />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const MessageBranchNext = ({
|
||||
children,
|
||||
...props
|
||||
}: MessageBranchNextProps) => {
|
||||
const { goToNext, totalBranches } = useMessageBranch();
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Next branch"
|
||||
disabled={totalBranches <= 1}
|
||||
onClick={goToNext}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon size={14} />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export const MessageBranchPage = ({
|
||||
className,
|
||||
...props
|
||||
}: MessageBranchPageProps) => {
|
||||
const { currentBranch, totalBranches } = useMessageBranch();
|
||||
|
||||
return (
|
||||
<ButtonGroupText
|
||||
className={cn(
|
||||
"border-none bg-transparent text-neutral-500 shadow-none dark:text-neutral-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{currentBranch + 1} of {totalBranches}
|
||||
</ButtonGroupText>
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
export const MessageResponse = memo(
|
||||
({ className, ...props }: MessageResponseProps) => (
|
||||
<Streamdown
|
||||
className={cn(
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||
className,
|
||||
)}
|
||||
plugins={{ code, mermaid, math, cjk }}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
(prevProps, nextProps) => prevProps.children === nextProps.children,
|
||||
);
|
||||
|
||||
MessageResponse.displayName = "MessageResponse";
|
||||
|
||||
export type MessageToolbarProps = ComponentProps<"div">;
|
||||
|
||||
export const MessageToolbar = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MessageToolbarProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-4 flex w-full items-center justify-between gap-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -62,7 +62,7 @@ export function Navbar() {
|
||||
<PreviewBanner branchName={previewBranchName} />
|
||||
) : null}
|
||||
<nav
|
||||
className="border-zinc-[#EFEFF0] inline-flex w-full items-center border border-[#EFEFF0] bg-[#F3F4F6]/20 p-3 backdrop-blur-[26px]"
|
||||
className="inline-flex w-full items-center border border-none bg-[#FAFAFA] p-3 backdrop-blur-[26px]"
|
||||
style={{ height: NAVBAR_HEIGHT_PX }}
|
||||
>
|
||||
{/* Left section */}
|
||||
|
||||
83
autogpt_platform/frontend/src/components/ui/button-group.tsx
Normal file
83
autogpt_platform/frontend/src/components/ui/button-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"shadow-xs flex items-center gap-2 rounded-md border border-neutral-200 bg-neutral-100 px-4 text-sm font-medium dark:border-neutral-800 dark:bg-neutral-800 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"relative !m-0 self-stretch bg-neutral-200 data-[orientation=vertical]:h-auto dark:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
};
|
||||
59
autogpt_platform/frontend/src/components/ui/button.tsx
Normal file
59
autogpt_platform/frontend/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
|
||||
destructive:
|
||||
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
|
||||
outline:
|
||||
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
|
||||
secondary:
|
||||
"bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
|
||||
ghost:
|
||||
"hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
|
||||
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
"icon-sm": "h-8 w-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
22
autogpt_platform/frontend/src/components/ui/input.tsx
Normal file
22
autogpt_platform/frontend/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-neutral-200 bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
31
autogpt_platform/frontend/src/components/ui/separator.tsx
Normal file
31
autogpt_platform/frontend/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-neutral-200 dark:bg-neutral-800",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
143
autogpt_platform/frontend/src/components/ui/sheet.tsx
Normal file
143
autogpt_platform/frontend/src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-neutral-950",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity data-[state=open]:bg-neutral-100 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none dark:ring-offset-neutral-950 dark:data-[state=open]:bg-neutral-800 dark:focus:ring-neutral-300">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold text-neutral-950 dark:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
781
autogpt_platform/frontend/src/components/ui/sidebar.tsx
Normal file
781
autogpt_platform/frontend/src/components/ui/sidebar.tsx
Normal file
@@ -0,0 +1,781 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "20rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "20rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = "SidebarProvider";
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
SidebarTrigger.displayName = "SidebarTrigger";
|
||||
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button">
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarRail.displayName = "SidebarRail";
|
||||
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"main">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 flex-col bg-white dark:bg-neutral-950",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInset.displayName = "SidebarInset";
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-white shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring dark:bg-neutral-950",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = "SidebarInput";
|
||||
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarHeader.displayName = "SidebarHeader";
|
||||
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarFooter.displayName = "SidebarFooter";
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = "SidebarSeparator";
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarContent.displayName = "SidebarContent";
|
||||
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroup.displayName = "SidebarGroup";
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenu.displayName = "SidebarMenu";
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-white shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))] dark:bg-neutral-950",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
18
autogpt_platform/frontend/src/components/ui/skeleton.tsx
Normal file
18
autogpt_platform/frontend/src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"animate-pulse rounded-md bg-neutral-900/10 dark:bg-neutral-50/10",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
32
autogpt_platform/frontend/src/components/ui/tooltip.tsx
Normal file
32
autogpt_platform/frontend/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md bg-neutral-900 px-3 py-1.5 text-xs text-neutral-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-neutral-50 dark:text-neutral-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
21
autogpt_platform/frontend/src/hooks/use-mobile.tsx
Normal file
21
autogpt_platform/frontend/src/hooks/use-mobile.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ const mockFlags = {
|
||||
[Flag.AGENT_FAVORITING]: false,
|
||||
[Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS,
|
||||
[Flag.ENABLE_PLATFORM_PAYMENT]: false,
|
||||
[Flag.CHAT]: false,
|
||||
[Flag.CHAT]: true,
|
||||
};
|
||||
|
||||
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {
|
||||
|
||||
@@ -65,6 +65,16 @@ const config = {
|
||||
"600": "#282828",
|
||||
"700": "#272727",
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
"0": "0rem",
|
||||
|
||||
Reference in New Issue
Block a user