This commit is contained in:
marko-kraemer 2025-04-16 01:47:20 +01:00
parent 6f64db2dad
commit 5d039cd87c
17 changed files with 440 additions and 1122 deletions

View File

@ -20,6 +20,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
@ -2392,6 +2393,36 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.4.tgz",
"integrity": "sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ==",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.3",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
@ -2462,7 +2493,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
@ -2480,7 +2510,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.0.tgz",
"integrity": "sha512-b1Sdc75s7zN9B8ONQTGBSHL3XS8+IcjcOIY51fhM4R1Hx8s0YbgqgyNZiri4qcYMVZK8hfCZVBiyCm7N9rs0rw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",

View File

@ -21,6 +21,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",

View File

@ -13,6 +13,9 @@ import { BillingErrorAlert } from "@/components/billing/BillingErrorAlert";
import { useBillingError } from "@/hooks/useBillingError";
import { MessageList } from "@/components/thread/message-list";
import { ChatInput } from "@/components/thread/chat-input";
import { ToolCallsSidebar } from "@/components/thread/tool-calls-sidebar";
import { useToolsPanel } from "@/hooks/use-tools-panel";
import { cn } from "@/lib/utils";
interface AgentPageProps {
params: {
@ -256,6 +259,7 @@ export default function AgentPage({ params }: AgentPageProps) {
const searchParams = useSearchParams();
const initialMessage = searchParams.get('message');
const streamCleanupRef = useRef<(() => void) | null>(null);
const { showPanel } = useToolsPanel();
// State
const [agent, setAgent] = useState<Project | null>(null);
@ -817,18 +821,24 @@ export default function AgentPage({ params }: AgentPageProps) {
}
return (
<ChatContainer
messages={messages}
streamContent={streamContent}
isStreaming={isStreaming}
isAgentRunning={isAgentRunning}
agent={agent}
onSendMessage={handleSendMessage}
isSending={isSending}
conversation={conversation}
onStopAgent={handleStopAgent}
userMessage={userMessage}
setUserMessage={setUserMessage}
/>
<div className={cn(
"transition-all duration-300",
showPanel && "pr-[400px]"
)}>
<ToolCallsSidebar />
<ChatContainer
messages={messages}
streamContent={streamContent}
isStreaming={isStreaming}
isAgentRunning={isAgentRunning}
agent={agent}
onSendMessage={handleSendMessage}
isSending={isSending}
conversation={conversation}
onStopAgent={handleStopAgent}
userMessage={userMessage}
setUserMessage={setUserMessage}
/>
</div>
);
}

View File

@ -11,6 +11,7 @@ import {
BreadcrumbPage,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import { SidebarRight } from "@/components/dashboard/sidebar/sidebar-right"
interface DashboardLayoutProps {
children: React.ReactNode

View File

@ -1,28 +0,0 @@
import DashboardLayout from "@/components/dashboard/old/DashboardLayout";
import { createClient } from "@/lib/supabase/server";
interface DashboardRootLayoutProps {
children: React.ReactNode;
}
export default async function DashboardRootLayout({
children,
}: DashboardRootLayoutProps) {
// Get the current user via Supabase
const supabaseClient = await createClient();
const { data: { user } } = await supabaseClient.auth.getUser();
// Get the personal account details
const { data: personalAccount } = await supabaseClient.rpc('get_personal_account');
return (
<DashboardLayout
accountId={personalAccount?.account_id || user.id}
userName={user?.user_metadata?.name || user.email?.split('@')[0] || 'User'}
userEmail={user.email}
rightPanelTitle="Suna's Computer"
>
{children}
</DashboardLayout>
);
}

View File

@ -1,85 +0,0 @@
"use client";
import React, { useState, Suspense } from 'react';
import { Skeleton } from "@/components/ui/skeleton";
import { useRouter } from 'next/navigation';
import { ChatInput } from '@/components/thread/chat-input';
import { createProject, addUserMessage, startAgent, createThread } from "@/lib/api";
function DashboardContent() {
const [inputValue, setInputValue] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const handleSubmit = async (message: string) => {
if (!message.trim() || isSubmitting) return;
setIsSubmitting(true);
try {
// 1. Create a new project with the message as the name
const newAgent = await createProject({
name: message.trim().length > 50
? message.trim().substring(0, 47) + "..."
: message.trim(),
description: "",
});
// 2. Create a new thread for this project
const thread = await createThread(newAgent.id);
// 3. Add the user message to the thread
await addUserMessage(thread.thread_id, message.trim());
// 4. Start the agent with the thread ID
const agentRun = await startAgent(thread.thread_id);
// 5. Navigate to the new agent's thread page
router.push(`/dashboard/agents/${thread.thread_id}`);
} catch (error) {
console.error("Error creating agent:", error);
setIsSubmitting(false);
}
};
return (
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="text-center mb-10">
<h1 className="text-4xl font-medium text-foreground mb-2">Hello.</h1>
<h2 className="text-2xl text-muted-foreground">What can I help with?</h2>
</div>
<ChatInput
onSubmit={handleSubmit}
loading={isSubmitting}
placeholder="Ask anything..."
value={inputValue}
onChange={setInputValue}
/>
</div>
</div>
);
}
export default function DashboardPage() {
return (
<Suspense fallback={
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="flex flex-col items-center text-center mb-10">
<Skeleton className="h-10 w-40 mb-2" />
<Skeleton className="h-7 w-56" />
</div>
<Skeleton className="w-full h-[100px] rounded-xl" />
<div className="flex justify-center mt-3">
<Skeleton className="h-5 w-16" />
</div>
</div>
</div>
}>
<DashboardContent />
</Suspense>
);
}

View File

@ -1,278 +0,0 @@
"use client";
import { ReactNode, useState, useEffect, useRef, useCallback } from "react";
import { useTheme } from "next-themes";
import { useToolsPanel } from "@/hooks/use-tools-panel";
// Import our new components
import LeftSidebar from "@/components/dashboard/old/LeftSidebar";
import Header from "@/components/dashboard/old/Header";
import MainContent from "@/components/dashboard/old/MainContent";
import RightSidebar from "@/components/dashboard/old/RightSidebar";
import FloatingPanel from "@/components/dashboard/old/FloatingPanel";
interface DashboardLayoutProps {
children: ReactNode;
accountId: string;
userName?: string;
userEmail?: string;
rightPanelContent?: ReactNode;
rightPanelTitle?: string;
}
export default function DashboardLayout({
children,
accountId,
userName,
userEmail,
rightPanelContent,
rightPanelTitle = "Details",
}: DashboardLayoutProps) {
// Initialize with default values
const [leftSidebarCollapsed, setLeftSidebarCollapsed] = useState(true);
const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(true);
const [showRightSidebar, setShowRightSidebar] = useState(false);
// State for draggable panel
const [position, setPosition] = useState({ x: 20, y: 400 });
const layoutInitialized = useRef(false);
// Use our tools panel hook
const {
showPanel,
renderToolsPanel,
toolCalls
} = useToolsPanel();
// Use tool panel as the right panel if it's available
const effectiveRightPanelContent = showPanel ? renderToolsPanel() : rightPanelContent;
const effectiveRightPanelTitle = showPanel ? `Suna's Computer (${toolCalls.length})` : rightPanelTitle;
// Update right sidebar visibility based on panel visibility
useEffect(() => {
if (showPanel) {
setShowRightSidebar(true);
setRightSidebarCollapsed(false);
} else if (!rightPanelContent) {
// If the tools panel is hidden and there's no other content, hide the sidebar
setShowRightSidebar(false);
}
}, [showPanel, rightPanelContent]);
// Set initial showRightSidebar based on rightPanelContent prop
useEffect(() => {
// Only set initial visibility, don't override user choices later
if (!layoutInitialized.current) {
setShowRightSidebar(!!effectiveRightPanelContent);
}
}, [effectiveRightPanelContent]);
// Load layout state from localStorage on initial render
useEffect(() => {
if (typeof window !== "undefined" && !layoutInitialized.current) {
try {
const savedLayout = localStorage.getItem("dashboardLayout");
if (savedLayout) {
const layout = JSON.parse(savedLayout);
setLeftSidebarCollapsed(layout.leftSidebarCollapsed);
if (effectiveRightPanelContent) {
setRightSidebarCollapsed(layout.rightSidebarCollapsed);
setShowRightSidebar(layout.showRightSidebar);
}
if (
layout.position &&
typeof layout.position.x === "number" &&
typeof layout.position.y === "number"
) {
setPosition(layout.position);
}
layoutInitialized.current = true;
}
} catch (error) {
console.error("Error loading layout from localStorage:", error);
}
}
}, [effectiveRightPanelContent]);
// Save layout state to localStorage whenever relevant state changes
useEffect(() => {
if (typeof window !== "undefined" && layoutInitialized.current) {
try {
const layoutState = {
leftSidebarCollapsed,
rightSidebarCollapsed,
showRightSidebar,
position,
};
localStorage.setItem("dashboardLayout", JSON.stringify(layoutState));
} catch (error) {
console.error("Error saving layout to localStorage:", error);
}
}
}, [leftSidebarCollapsed, rightSidebarCollapsed, showRightSidebar, position]);
// Toggle left sidebar and ensure right sidebar is collapsed when left is expanded
const toggleLeftSidebar = useCallback(() => {
setLeftSidebarCollapsed(!leftSidebarCollapsed);
if (!leftSidebarCollapsed) {
// When collapsing left, we don't need to do anything
} else {
// When expanding left, collapse the right sidebar
setRightSidebarCollapsed(true);
}
}, [leftSidebarCollapsed, setLeftSidebarCollapsed, setRightSidebarCollapsed]);
// Toggle right sidebar and ensure left sidebar is collapsed when right is expanded
const toggleRightSidebar = useCallback(() => {
setRightSidebarCollapsed(!rightSidebarCollapsed);
if (!rightSidebarCollapsed) {
// When collapsing right, we don't need to do anything
} else {
// When expanding right, collapse the left sidebar
setLeftSidebarCollapsed(true);
}
}, [
rightSidebarCollapsed,
setRightSidebarCollapsed,
setLeftSidebarCollapsed,
]);
// Toggle right sidebar visibility
const toggleRightSidebarVisibility = useCallback(() => {
const newVisibility = !showRightSidebar;
setShowRightSidebar(newVisibility);
if (newVisibility) {
// When showing the sidebar, expand it and collapse the left sidebar
setRightSidebarCollapsed(false);
setLeftSidebarCollapsed(true);
} else {
// When hiding the sidebar, make sure it's fully collapsed
setRightSidebarCollapsed(true);
}
}, [
showRightSidebar,
setShowRightSidebar,
setRightSidebarCollapsed,
setLeftSidebarCollapsed,
]);
// Update position based on window height (client-side only)
useEffect(() => {
if (typeof window !== "undefined") {
setPosition({ x: 20, y: window.innerHeight - 300 });
}
}, []);
// Keyboard shortcuts for sidebar toggling
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Check if Command/Ctrl key is pressed
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const modifierKey = isMac ? e.metaKey : e.ctrlKey;
if (modifierKey) {
// Ctrl/Cmd + B for left sidebar
if (e.key === "b") {
e.preventDefault();
toggleLeftSidebar();
}
// Ctrl/Cmd + I for right sidebar
if (e.key === "i") {
e.preventDefault();
if (showRightSidebar) {
toggleRightSidebar();
} else {
toggleRightSidebarVisibility();
}
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [
toggleLeftSidebar,
toggleRightSidebar,
toggleRightSidebarVisibility,
showRightSidebar,
]);
// Render floating panel if needed
const renderFloatingPanelComponent = () => {
if (showRightSidebar === true && rightSidebarCollapsed === true && effectiveRightPanelContent) {
return (
<FloatingPanel
title={effectiveRightPanelTitle}
position={position}
setPosition={setPosition}
toggleSidebar={toggleRightSidebar}
toggleVisibility={toggleRightSidebarVisibility}
/>
);
}
return null;
};
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground relative">
{/* Left Sidebar */}
<LeftSidebar
accountId={accountId}
userName={userName}
userEmail={userEmail}
isCollapsed={leftSidebarCollapsed}
toggleCollapsed={toggleLeftSidebar}
/>
{/* Layout container for main content and right panel */}
<div className="flex flex-1 relative w-full">
{/* Main Content with separate Header */}
<div
className={`flex flex-col ${
showRightSidebar && !rightSidebarCollapsed
? "flex-1"
: "w-full"
}`}
>
<Header
userName={userName}
userEmail={userEmail}
showRightSidebar={showRightSidebar}
effectiveRightPanelContent={effectiveRightPanelContent}
effectiveRightPanelTitle={effectiveRightPanelTitle}
showPanel={showPanel}
toggleRightSidebarVisibility={toggleRightSidebarVisibility}
/>
<MainContent
leftSidebarCollapsed={leftSidebarCollapsed}
rightSidebarCollapsed={rightSidebarCollapsed}
showRightSidebar={showRightSidebar}
showFloatingPanel={showRightSidebar && rightSidebarCollapsed && !!effectiveRightPanelContent}
renderFloatingPanel={renderFloatingPanelComponent}
>
{children}
</MainContent>
</div>
{/* Right Sidebar */}
{showRightSidebar === true && effectiveRightPanelContent && (
<RightSidebar
isCollapsed={rightSidebarCollapsed}
panelTitle={effectiveRightPanelTitle}
panelContent={effectiveRightPanelContent}
toggleSidebar={toggleRightSidebar}
toggleVisibility={toggleRightSidebarVisibility}
/>
)}
</div>
</div>
);
}

View File

@ -1,147 +0,0 @@
"use client";
import React, { useRef, useState, useEffect, useCallback } from "react";
import { X, Maximize2, GripHorizontal } from "lucide-react";
interface FloatingPanelProps {
title: string;
position: { x: number; y: number };
setPosition: (position: { x: number; y: number }) => void;
toggleSidebar: () => void;
toggleVisibility: () => void;
}
export default function FloatingPanel({
title,
position,
setPosition,
toggleSidebar,
toggleVisibility
}: FloatingPanelProps) {
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const floatingPanelRef = useRef<HTMLDivElement>(null);
// Dragging functionality
const handleMouseDown = (e: React.MouseEvent) => {
if (
!(e.target as HTMLElement).closest(".drag-handle") ||
(e.target as HTMLElement).closest("button")
) {
return;
}
e.preventDefault();
setIsDragging(true);
const rect = floatingPanelRef.current?.getBoundingClientRect();
if (rect) {
setDragStart({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}
};
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging || !floatingPanelRef.current) return;
e.preventDefault();
const mainContent = document.querySelector(".main-content-area");
if (!mainContent) return;
const mainRect = mainContent.getBoundingClientRect();
const panelRect = floatingPanelRef.current.getBoundingClientRect();
let newX = e.clientX - dragStart.x - mainRect.left;
let newY = e.clientY - dragStart.y - mainRect.top;
// Constrain within bounds
newX = Math.max(0, Math.min(newX, mainRect.width - panelRect.width));
newY = Math.max(0, Math.min(newY, mainRect.height - panelRect.height));
setPosition({ x: newX, y: newY });
},
[isDragging, dragStart, setPosition]
);
const handleMouseUp = () => {
setIsDragging(false);
};
// Add and remove event listeners
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, handleMouseMove]);
return (
<div
ref={floatingPanelRef}
className={`absolute bg-background border border-subtle dark:border-white/10 rounded-lg shadow-custom flex flex-col w-72 overflow-hidden ${
isDragging ? "cursor-grabbing" : ""
}`}
style={{
left: `${position.x}px`,
top: `${position.y}px`,
backdropFilter: "blur(8px)",
zIndex: 50,
}}
onMouseDown={handleMouseDown}
>
<div className="p-2 flex items-center justify-between drag-handle cursor-grab">
<div className="flex items-center gap-1.5">
<GripHorizontal
size={12}
className="text-icon-color"
/>
<h3 className="font-medium text-foreground text-xs select-none">
{title}
</h3>
</div>
<div className="flex items-center gap-1">
<button
onClick={toggleSidebar}
className="p-1 rounded-full hover:bg-hover-bg text-foreground/60 transition-all duration-200"
aria-label="Expand panel"
>
<Maximize2 size={12} />
</button>
<button
onClick={toggleVisibility}
className="p-1 rounded-full hover:bg-hover-bg text-foreground/60 transition-all duration-200"
aria-label="Close panel"
>
<X size={12} />
</button>
</div>
</div>
<div className="p-3 max-h-60 overflow-y-auto">
<div className="flex flex-col items-center justify-center py-4 text-foreground/40">
<p className="mb-2 select-none text-xs">Content minimized</p>
<button
onClick={toggleSidebar}
className="px-2 py-1 rounded-md text-primary text-xs"
>
Expand
</button>
</div>
</div>
<div
className="absolute inset-x-0 bottom-0 h-[40%] pointer-events-none bg-gradient-overlay"
style={{
opacity: 0.4,
mixBlendMode: "multiply",
}}
/>
</div>
);
}

View File

@ -1,124 +0,0 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { signOut } from "@/app/auth/actions";
import { useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import {
Sun,
Moon,
UserIcon,
Laptop
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuGroup,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
interface HeaderProps {
userName?: string;
userEmail?: string;
showRightSidebar: boolean;
effectiveRightPanelContent: React.ReactNode;
effectiveRightPanelTitle: string;
showPanel: boolean;
toggleRightSidebarVisibility: () => void;
}
export default function Header({
userName,
userEmail,
showRightSidebar,
effectiveRightPanelContent,
effectiveRightPanelTitle,
showPanel,
toggleRightSidebarVisibility
}: HeaderProps) {
const { theme, setTheme } = useTheme();
const router = useRouter();
const handleSignOut = async () => {
await signOut();
router.refresh();
};
return (
<header className="h-12 px-4 flex items-center justify-end">
<div className="flex items-center gap-3">
{!showRightSidebar && effectiveRightPanelContent && (
<button
onClick={toggleRightSidebarVisibility}
className="px-3 py-1.5 rounded-full hover:bg-hover-bg text-foreground/80 hover:text-foreground transition-all duration-200 text-xs border border-subtle dark:border-white/10"
>
{showPanel ? (
<span className="flex items-center gap-1">
<Laptop className="h-3 w-3" />
{`Suna's Computer`}
</span>
) : (
effectiveRightPanelTitle
)}
</button>
)}
{/* User dropdown in header - always show */}
<div className="flex-shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="relative h-8 w-8 rounded-full border border-subtle dark:border-white/10 hover:bg-hover-bg flex items-center justify-center">
<UserIcon className="h-4 w-4 text-foreground/80" />
<span className="sr-only">User menu</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 border-subtle dark:border-white/10 bg-card-bg dark:bg-background-secondary rounded-xl shadow-custom" align="end" forceMount>
<DropdownMenuLabel className="font-normal border-b border-subtle dark:border-white/10">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none text-foreground">{userName}</p>
{userEmail && (
<p className="text-xs leading-none text-foreground/70">
{userEmail}
</p>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuGroup className="p-1">
<DropdownMenuItem asChild className="rounded-md hover:!bg-[#f1eee7] dark:hover:!bg-[#141413] cursor-pointer">
<Link href="/dashboard" className="flex w-full h-full text-foreground/90">My Account</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="rounded-md hover:!bg-[#f1eee7] dark:hover:!bg-[#141413] cursor-pointer">
<Link href="/dashboard/settings" className="flex w-full h-full text-foreground/90">Settings</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="rounded-md hover:!bg-[#f1eee7] dark:hover:!bg-[#141413] cursor-pointer">
<Link href="/dashboard/settings/teams" className="flex w-full h-full text-foreground/90">Teams</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator className="border-subtle dark:border-white/10" />
<div className="p-1">
<DropdownMenuItem asChild className="rounded-md hover:!bg-[#f1eee7] dark:hover:!bg-[#141413] cursor-pointer">
<button onClick={handleSignOut} className="flex w-full h-full px-2 py-1.5 text-left text-foreground/90">Log out</button>
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="relative">
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="relative h-8 w-8 rounded-full border border-subtle dark:border-white/10 hover:bg-hover-bg flex items-center justify-center"
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
<span className="sr-only">Toggle theme</span>
</button>
</div>
</div>
</header>
);
}

View File

@ -1,225 +0,0 @@
"use client";
import Link from "next/link";
import { Search, MessagesSquare, ArrowRight, PanelLeft, X } from "lucide-react";
import { useState, useEffect } from "react";
import UserAccountPanel from "@/components/dashboard/old/user-account-panel";
import NavItem from "@/components/dashboard/old/NavItem";
import { getProjects, getThreads } from "@/lib/api";
interface LeftSidebarProps {
accountId: string;
userName?: string;
userEmail?: string;
isCollapsed: boolean;
toggleCollapsed: () => void;
}
export default function LeftSidebar({
accountId,
userName,
userEmail,
isCollapsed,
toggleCollapsed,
}: LeftSidebarProps) {
// Add search state for agents
const [searchQuery, setSearchQuery] = useState("");
const [filteredAgents, setFilteredAgents] = useState<{name: string, href: string}[]>([]);
const [showSearchInput, setShowSearchInput] = useState(false);
const [agents, setAgents] = useState<{name: string, href: string}[]>([]);
const [isLoadingAgents, setIsLoadingAgents] = useState(true);
// Filter agents when search query changes
useEffect(() => {
if (searchQuery.trim() === "") {
setFilteredAgents(agents);
} else {
const filtered = agents.filter(agent =>
agent.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredAgents(filtered);
}
}, [searchQuery, agents]);
// Load agents dynamically from the API
useEffect(() => {
async function loadAgents() {
try {
const projectsData = await getProjects();
const agentsList = [];
const seenThreadIds = new Set(); // Track unique thread IDs
for (const project of projectsData) {
const threads = await getThreads(project.id);
if (threads && threads.length > 0) {
// For each thread in the project, create an agent entry
for (const thread of threads) {
// Only add if we haven't seen this thread ID before
if (!seenThreadIds.has(thread.thread_id)) {
seenThreadIds.add(thread.thread_id);
agentsList.push({
name: `${project.name} - ${thread.thread_id.slice(0, 4)}`,
href: `/dashboard/agents/${thread.thread_id}`
});
}
}
}
}
// Sort by most recent
setAgents(agentsList);
} catch (err) {
console.error("Error loading agents for sidebar:", err);
} finally {
setIsLoadingAgents(false);
}
}
loadAgents();
}, []);
// Get only the latest 20 agents for the sidebar
const recentAgents = filteredAgents.slice(0, 20);
return (
<div
className={`transition-all duration-300 ease-in-out ${
!isCollapsed ? "w-56" : "w-12"
}`}
>
<div
className="flex flex-col h-full bg-background"
style={{ backdropFilter: "blur(8px)" }}
>
<div className="h-12 p-2 flex items-center justify-between">
<div className={`font-medium text-sm text-card-title ${isCollapsed ? "hidden" : "block"}`}>
<Link href="/">AgentPress</Link>
</div>
<button
onClick={toggleCollapsed}
className={`p-1 hover:bg-hover-bg text-foreground/60 transition-all duration-200 ${isCollapsed ? "mx-auto" : ""}`}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
<PanelLeft size={14} className={`transition-transform duration-200 ${isCollapsed ? "rotate-180" : ""}`} />
</button>
</div>
<div className="px-2 pb-2 border-b border-subtle dark:border-white/10">
<UserAccountPanel
accountId={accountId}
userName={userName}
userEmail={userEmail}
isCollapsed={isCollapsed}
/>
</div>
<div className="flex-1 overflow-y-auto pt-2">
{/* Agents Section */}
<div className="py-1 px-2 mt-2">
<div className={`flex justify-between items-center mb-1 ${isCollapsed ? "hidden" : "block"}`}>
<Link href="/dashboard/agents" className="text-xs font-medium text-foreground/50">Agents</Link>
<div className="flex items-center gap-1">
{!isCollapsed && (
<button
onClick={() => setShowSearchInput(!showSearchInput)}
className="text-xs flex items-center justify-center h-5 w-5 rounded-md bg-background-secondary/80 hover:bg-hover-bg text-foreground/50 hover:text-foreground transition-colors duration-200 border border-subtle/40 dark:border-white/5"
aria-label="Search agents"
>
<Search size={12} className="text-icon-color" />
</button>
)}
<Link
href="/dashboard"
className="text-xs flex items-center justify-center px-1.5 h-5 rounded-md bg-background-secondary/80 hover:bg-hover-bg text-foreground/50 hover:text-foreground transition-colors duration-200 border border-subtle/40 dark:border-white/5"
>
+ New
</Link>
</div>
</div>
{/* Add search input for agents - only show when search is clicked */}
{!isCollapsed && (
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
showSearchInput ? 'max-h-10 opacity-100 mb-2' : 'max-h-0 opacity-0 mb-0'
}`}
>
<div className={`relative transition-transform duration-300 ${
showSearchInput ? 'translate-y-0' : '-translate-y-4'
}`}>
<div className="absolute inset-y-0 left-0 pl-2 flex items-center pointer-events-none">
<Search size={12} className="text-icon-color" />
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search agents..."
className="w-full py-1 pl-7 pr-2 text-xs bg-background-secondary border border-subtle dark:border-white/10 rounded-md placeholder-foreground/40 focus:outline-none focus:ring-1 focus:ring-primary"
autoFocus={showSearchInput}
tabIndex={showSearchInput ? 0 : -1}
/>
{searchQuery && (
<button
className="absolute inset-y-0 right-0 pr-2 flex items-center"
onClick={() => setSearchQuery("")}
>
<X size={12} className="text-icon-color" />
</button>
)}
</div>
</div>
)}
<div className="space-y-0.5">
{isLoadingAgents ? (
// Show skeleton loaders while loading
Array.from({length: 3}).map((_, index) => (
<div key={index} className="flex items-center gap-2 py-1.5 px-2">
{!isCollapsed && (
<div className="w-4 h-4 bg-foreground/10 rounded-md animate-pulse"></div>
)}
{!isCollapsed && (
<div className="h-3 bg-foreground/10 rounded w-3/4 animate-pulse"></div>
)}
</div>
))
) : recentAgents.length > 0 ? (
// Show only the latest 20 agents
<>
{recentAgents.map((agent, index) => (
<NavItem
key={index}
icon={<MessagesSquare size={16} />}
label={agent.name}
href={agent.href}
isCollapsed={isCollapsed}
hideIconWhenCollapsed={true}
/>
))}
{/* "See all agents" link */}
{filteredAgents.length > 20 && (
<Link
href="/dashboard/agents"
className={`flex items-center gap-1 py-1.5 px-2 text-xs text-foreground/60 hover:text-foreground hover:bg-hover-bg transition-all ${
isCollapsed ? "justify-center" : ""
}`}
>
{!isCollapsed && <span>See all agents</span>}
<ArrowRight size={12} />
</Link>
)}
</>
) : (
// Show empty state with search query info if applicable
<div className={`text-xs text-foreground/50 p-2 ${isCollapsed ? "hidden" : "block"}`}>
{searchQuery ? `No agents matching "${searchQuery}"` : "No agents yet"}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,47 +0,0 @@
"use client";
import React from "react";
interface MainContentProps {
children: React.ReactNode;
leftSidebarCollapsed: boolean;
rightSidebarCollapsed: boolean;
showRightSidebar: boolean;
showFloatingPanel: boolean;
renderFloatingPanel?: () => React.ReactNode;
}
export default function MainContent({
children,
leftSidebarCollapsed,
rightSidebarCollapsed,
showRightSidebar,
showFloatingPanel,
renderFloatingPanel
}: MainContentProps) {
return (
<div
className="flex flex-col h-full overflow-hidden bg-background-secondary transition-all duration-300 ease-in-out rounded-l-xl shadow-sm border-l border-subtle dark:border-white/10"
style={{
backdropFilter: "blur(8px)",
zIndex: 10
}}
>
<div className="flex-1 overflow-y-auto bg-transparent main-content-area relative">
<div
className={`mx-auto p-4 transition-all duration-300 ${
// Increase container width when sidebars are collapsed
leftSidebarCollapsed &&
(rightSidebarCollapsed || !showRightSidebar)
? "container max-w-[95%]"
: "container"
}`}
>
{children}
</div>
{/* Floating panel rendered here if needed */}
{showFloatingPanel && renderFloatingPanel && renderFloatingPanel()}
</div>
</div>
);
}

View File

@ -1,42 +0,0 @@
"use client";
import Link from "next/link";
import React from "react";
interface NavItemProps {
icon: React.ReactNode;
label: string;
href: string;
isCollapsed: boolean;
hideIconWhenCollapsed?: boolean;
}
export default function NavItem({
icon,
label,
href,
isCollapsed,
hideIconWhenCollapsed = false
}: NavItemProps) {
return (
<Link
href={href}
className={`flex items-center gap-2 py-1.5 px-2 hover:bg-hover-bg transition-all duration-200 group text-sm ${
isCollapsed ? "justify-center" : ""
}`}
>
{(!isCollapsed || !hideIconWhenCollapsed) && (
<div className="text-icon-color flex-shrink-0">
{icon}
</div>
)}
{!isCollapsed && <span className="text-foreground/90 truncate">{label}</span>}
{isCollapsed && !hideIconWhenCollapsed && (
<div className="absolute left-full ml-2 scale-0 group-hover:scale-100 transition-all duration-200 origin-left z-50">
<div className="bg-background p-2 shadow-custom border border-subtle dark:border-white/10">
<span className="whitespace-nowrap text-foreground text-xs">{label}</span>
</div>
</div>
)}
</Link>
);
}

View File

@ -1,70 +0,0 @@
"use client";
import React from "react";
import { X, Minimize2 } from "lucide-react";
interface RightSidebarProps {
isCollapsed: boolean;
panelTitle: string;
panelContent: React.ReactNode;
toggleSidebar: () => void;
toggleVisibility: () => void;
}
export default function RightSidebar({
isCollapsed,
panelTitle,
panelContent,
toggleSidebar,
toggleVisibility
}: RightSidebarProps) {
return (
<div
className={`h-screen border-l border-subtle dark:border-white/10 bg-background transition-all duration-300 ease-in-out rounded-l-xl shadow-custom ${
isCollapsed
? "w-0 opacity-0 p-0 m-0 border-0"
: "w-[40%] opacity-100 flex-shrink-0"
} overflow-hidden`}
style={{
backdropFilter: "blur(8px)",
zIndex: 20
}}
>
<div className="h-12 p-2 flex items-center justify-between rounded-t-xl">
<h2 className="font-medium text-sm text-card-title">
{panelTitle}
</h2>
<div className="flex items-center gap-1">
<button
onClick={toggleSidebar}
className="p-1 hover:bg-hover-bg text-foreground/60 transition-all duration-200"
aria-label="Collapse sidebar"
>
<Minimize2 size={14} />
</button>
<button
onClick={toggleVisibility}
className="p-1 hover:bg-hover-bg text-foreground/60 transition-all duration-200"
aria-label="Close sidebar"
>
<X size={14} />
</button>
</div>
</div>
<div className="p-3 overflow-y-auto h-[calc(100vh-48px)]">
{panelContent || (
<div className="flex flex-col items-center justify-center h-full text-foreground/40">
<p className="text-sm">No content selected</p>
</div>
)}
</div>
<div
className="absolute inset-x-0 bottom-0 h-[40%] pointer-events-none bg-gradient-overlay"
style={{
opacity: 0.4,
mixBlendMode: "multiply",
}}
/>
</div>
);
}

View File

@ -1,51 +0,0 @@
'use client';
import { useRouter } from "next/navigation";
import AccountSelector from "@/components/basejump/account-selector";
interface UserAccountPanelProps {
accountId: string;
userName?: string;
userEmail?: string;
isCollapsed?: boolean;
}
export default function UserAccountPanel({
accountId,
userName = "Account",
userEmail = "",
isCollapsed = false
}: UserAccountPanelProps) {
const router = useRouter();
if (isCollapsed) {
return (
<div className="flex flex-col items-center space-y-2">
<div className="w-12 h-12 shadow-custom border border-subtle dark:border-white/10 bg-card-bg dark:bg-background-secondary rounded-2xl overflow-hidden flex items-center justify-center">
<AccountSelector
accountId={accountId}
onAccountSelected={(account) => router.push(account?.personal_account ? `/dashboard` : `/dashboard/${account?.slug}`)}
className="w-10 h-10 rounded-full border-0 justify-center !p-0"
/>
</div>
{/* Other UI elements for collapsed mode can remain but we'll keep it minimal */}
<div className="hidden">
{/* User dropdown removed from here */}
</div>
</div>
);
}
return (
<div className="py-3">
{/* Only Account Selector */}
<AccountSelector
accountId={accountId}
onAccountSelected={(account) => router.push(account?.personal_account ? `/dashboard` : `/dashboard/${account?.slug}`)}
/>
{/* User dropdown removed */}
</div>
);
}

View File

@ -0,0 +1,306 @@
'use client';
import React from 'react';
import { useToolsPanel } from '@/hooks/use-tools-panel';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { ChevronLeft, ChevronRight, Grid3X3, Maximize2, Minimize2, PanelRightClose, Terminal, FileText, Search, Globe, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { getComponentForTag } from '@/components/thread/tool-components';
import { ParsedTag } from '@/lib/types/tool-calls';
import { motion, AnimatePresence } from "framer-motion";
import { Badge } from '@/components/ui/badge';
interface ToolCallsSidebarProps {
className?: string;
}
interface CombinedToolCall {
toolCall: ParsedTag; // From assistant message
toolResult: ParsedTag | null; // From tool message
id: string;
timestamp: number;
}
const ToolIcon = ({ type }: { type: string }) => {
if (type.includes('file')) return <FileText className="h-3.5 w-3.5" />;
if (type.includes('command')) return <Terminal className="h-3.5 w-3.5" />;
if (type.includes('search')) return <Search className="h-3.5 w-3.5" />;
if (type.includes('browser')) return <Globe className="h-3.5 w-3.5" />;
return <Terminal className="h-3.5 w-3.5" />;
};
const ToolStatus = ({ status }: { status: string }) => {
return (
<Badge variant={status === 'completed' ? 'default' : status === 'running' ? 'secondary' : 'destructive'} className="h-5">
{status === 'running' && (
<div className="mr-1 h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />
)}
{status === 'completed' ? 'Done' : status === 'running' ? 'Running' : 'Failed'}
</Badge>
);
};
export function ToolCallsSidebar({ className }: ToolCallsSidebarProps) {
const {
toolCalls,
showPanel,
setShowPanel,
currentToolIndex,
setCurrentToolIndex,
nextTool,
prevTool,
} = useToolsPanel();
const [isExpanded, setIsExpanded] = React.useState(false);
const [viewMode, setViewMode] = React.useState<'single' | 'grid'>('single');
// Combine tool calls with their results
const combinedToolCalls = React.useMemo(() => {
const combined: CombinedToolCall[] = [];
const processedIds = new Set<string>();
toolCalls.forEach((tag, index) => {
// Skip if we've already processed this tag
if (processedIds.has(tag.id)) return;
// Look for matching result in subsequent tags
// A result is a tag with the same name in a tool message
const nextTags = toolCalls.slice(index + 1);
const matchingResult = nextTags.find(nextTag =>
nextTag.tagName === tag.tagName &&
!processedIds.has(nextTag.id) &&
// Match attributes if they exist
(!tag.attributes || Object.entries(tag.attributes).every(([key, value]) =>
nextTag.attributes?.[key] === value
))
);
if (matchingResult) {
processedIds.add(matchingResult.id);
}
combined.push({
toolCall: tag,
toolResult: matchingResult || null,
id: tag.id,
timestamp: tag.timestamp || Date.now()
});
processedIds.add(tag.id);
});
return combined;
}, [toolCalls]);
// No need to render if there are no tool calls
if (combinedToolCalls.length === 0) {
return null;
}
const toggleViewMode = () => {
setViewMode(viewMode === 'single' ? 'grid' : 'single');
};
const renderToolCall = (combined: CombinedToolCall, mode: 'compact' | 'detailed') => {
const { toolCall, toolResult } = combined;
const isCompleted = !!toolResult;
if (mode === 'compact') {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<ToolIcon type={toolCall.tagName} />
<span className="text-sm font-medium">{toolCall.tagName}</span>
<div className="flex-1" />
<ToolStatus status={isCompleted ? 'completed' : 'running'} />
</div>
<div className="text-xs text-muted-foreground line-clamp-2">
{toolCall.content}
</div>
{toolResult && (
<>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<ArrowRight className="h-3 w-3" />
<span>Result</span>
</div>
<div className="text-xs text-muted-foreground line-clamp-2">
{toolResult.content}
</div>
</>
)}
</div>
);
}
return (
<div className="space-y-4">
<div className="rounded-lg border bg-card text-card-foreground">
<div className="flex items-center justify-between border-b px-4 py-2">
<div className="flex items-center gap-2">
<ToolIcon type={toolCall.tagName} />
<span className="text-sm font-medium">{toolCall.tagName}</span>
</div>
<ToolStatus status={isCompleted ? 'completed' : 'running'} />
</div>
<div className="p-4 text-sm font-mono">
{toolCall.content}
</div>
</div>
{toolResult && (
<div className="rounded-lg border bg-card text-card-foreground">
<div className="flex items-center gap-2 border-b px-4 py-2">
<ArrowRight className="h-3.5 w-3.5" />
<span className="text-sm font-medium">Result</span>
</div>
<div className="p-4 text-sm font-mono">
{toolResult.content}
</div>
</div>
)}
</div>
);
};
return (
<AnimatePresence>
{showPanel && (
<motion.aside
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className={cn(
"fixed right-0 top-0 z-30 flex h-screen flex-col border-l bg-background/80 backdrop-blur-md",
isExpanded ? "w-[600px]" : "w-[400px]",
className
)}
>
{/* Toggle button */}
<Button
variant="ghost"
size="icon"
className="absolute -left-10 top-4 h-8 w-8 rounded-l-md border-y border-l bg-background/80 backdrop-blur-md shadow-sm hover:bg-background/90 transition-colors"
onClick={() => setShowPanel(!showPanel)}
>
<PanelRightClose className={cn(
"h-4 w-4 transition-transform duration-200",
!showPanel && "rotate-180"
)} />
</Button>
{/* Header */}
<div className="flex items-center justify-between border-b bg-background/50 px-4 py-3">
<div className="flex items-center gap-3">
<h3 className="text-sm font-medium">Tool Calls</h3>
<Badge variant="outline" className="h-5">
{currentToolIndex + 1}/{combinedToolCalls.length}
</Badge>
</div>
<div className="flex items-center gap-1.5">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={toggleViewMode}
title={viewMode === 'single' ? 'Switch to grid view' : 'Switch to single view'}
>
<Grid3X3 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => setIsExpanded(!isExpanded)}
title={isExpanded ? 'Collapse panel' : 'Expand panel'}
>
{isExpanded ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</Button>
</div>
</div>
{/* Content */}
<ScrollArea className="flex-1">
<div className="p-4">
{viewMode === 'single' ? (
<div className="space-y-4">
{renderToolCall(combinedToolCalls[currentToolIndex], 'detailed')}
{/* Navigation */}
{combinedToolCalls.length > 1 && (
<div className="flex items-center gap-2 mt-6">
<Button
variant="outline"
size="sm"
onClick={prevTool}
disabled={currentToolIndex === 0}
className="flex-1"
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={nextTool}
disabled={currentToolIndex === combinedToolCalls.length - 1}
className="flex-1"
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
)}
</div>
) : (
<div className="grid grid-cols-1 gap-3">
{combinedToolCalls.map((toolCall, index) => (
<motion.div
key={toolCall.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className={cn(
"cursor-pointer transition-all border rounded-lg p-3 hover:shadow-sm",
index === currentToolIndex
? "border-primary bg-primary/5"
: "border-border hover:border-primary/40 hover:bg-muted/40"
)}
onClick={() => {
setCurrentToolIndex(index);
setViewMode('single');
}}
>
{renderToolCall(toolCall, 'compact')}
</motion.div>
))}
</div>
)}
</div>
</ScrollArea>
{/* Progress bar */}
{viewMode === 'single' && combinedToolCalls.length > 1 && (
<div className="px-4 py-3 border-t bg-background/50">
<div className="flex items-center gap-2">
<input
type="range"
min={0}
max={combinedToolCalls.length - 1}
value={currentToolIndex}
onChange={(e) => setCurrentToolIndex(parseInt(e.target.value))}
className="flex-1"
/>
</div>
</div>
)}
</motion.aside>
)}
</AnimatePresence>
);
}

View File

@ -1,20 +1,22 @@
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 badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
@ -23,13 +25,21 @@ const badgeVariants = cva(
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }