mirror of https://github.com/kortix-ai/suna.git
temp
This commit is contained in:
parent
6f64db2dad
commit
5d039cd87c
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
Loading…
Reference in New Issue