diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ffb3c17b..7e7c4031 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 9d8cef13..ca3e5e5b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/app/dashboard/agents/[threadId]/page.tsx b/frontend/src/app/dashboard/agents/[threadId]/page.tsx index c246a23e..e8c3c827 100644 --- a/frontend/src/app/dashboard/agents/[threadId]/page.tsx +++ b/frontend/src/app/dashboard/agents/[threadId]/page.tsx @@ -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(null); @@ -817,18 +821,24 @@ export default function AgentPage({ params }: AgentPageProps) { } return ( - +
+ + +
); } \ No newline at end of file diff --git a/frontend/src/app/dashboard/layout.tsx b/frontend/src/app/dashboard/layout.tsx index 1ba34f4f..5881b0a1 100644 --- a/frontend/src/app/dashboard/layout.tsx +++ b/frontend/src/app/dashboard/layout.tsx @@ -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 diff --git a/frontend/src/app/dashboard/layout_old.tsx b/frontend/src/app/dashboard/layout_old.tsx deleted file mode 100644 index 252c9896..00000000 --- a/frontend/src/app/dashboard/layout_old.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -} \ No newline at end of file diff --git a/frontend/src/app/dashboard/page_old.tsx b/frontend/src/app/dashboard/page_old.tsx deleted file mode 100644 index 2192594f..00000000 --- a/frontend/src/app/dashboard/page_old.tsx +++ /dev/null @@ -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 ( -
-
-
-

Hello.

-

What can I help with?

-
- - -
-
- ); -} - -export default function DashboardPage() { - return ( - -
-
- - -
- - -
- -
-
- - }> - -
- ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/DashboardLayout.tsx b/frontend/src/components/dashboard/old/DashboardLayout.tsx deleted file mode 100644 index e289dd30..00000000 --- a/frontend/src/components/dashboard/old/DashboardLayout.tsx +++ /dev/null @@ -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 ( - - ); - } - return null; - }; - - return ( -
- {/* Left Sidebar */} - - - {/* Layout container for main content and right panel */} -
- {/* Main Content with separate Header */} -
-
- - - {children} - -
- - {/* Right Sidebar */} - {showRightSidebar === true && effectiveRightPanelContent && ( - - )} -
-
- ); -} diff --git a/frontend/src/components/dashboard/old/FloatingPanel.tsx b/frontend/src/components/dashboard/old/FloatingPanel.tsx deleted file mode 100644 index bb109996..00000000 --- a/frontend/src/components/dashboard/old/FloatingPanel.tsx +++ /dev/null @@ -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(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 ( -
-
-
- -

- {title} -

-
-
- - -
-
- -
-
-

Content minimized

- -
-
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/Header.tsx b/frontend/src/components/dashboard/old/Header.tsx deleted file mode 100644 index e29945c6..00000000 --- a/frontend/src/components/dashboard/old/Header.tsx +++ /dev/null @@ -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 ( -
-
- {!showRightSidebar && effectiveRightPanelContent && ( - - )} - - {/* User dropdown in header - always show */} -
- - - - - - -
-

{userName}

- {userEmail && ( -

- {userEmail} -

- )} -
-
- - - My Account - - - Settings - - - Teams - - - -
- - - -
-
-
-
- -
- -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/LeftSidebar.tsx b/frontend/src/components/dashboard/old/LeftSidebar.tsx deleted file mode 100644 index 591d38ac..00000000 --- a/frontend/src/components/dashboard/old/LeftSidebar.tsx +++ /dev/null @@ -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 ( -
-
-
-
- AgentPress -
- -
- -
- -
- -
- {/* Agents Section */} -
-
- Agents -
- {!isCollapsed && ( - - )} - - + New - -
-
- - {/* Add search input for agents - only show when search is clicked */} - {!isCollapsed && ( -
-
-
- -
- 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 && ( - - )} -
-
- )} - -
- {isLoadingAgents ? ( - // Show skeleton loaders while loading - Array.from({length: 3}).map((_, index) => ( -
- {!isCollapsed && ( -
- )} - {!isCollapsed && ( -
- )} -
- )) - ) : recentAgents.length > 0 ? ( - // Show only the latest 20 agents - <> - {recentAgents.map((agent, index) => ( - } - label={agent.name} - href={agent.href} - isCollapsed={isCollapsed} - hideIconWhenCollapsed={true} - /> - ))} - - {/* "See all agents" link */} - {filteredAgents.length > 20 && ( - - {!isCollapsed && See all agents} - - - )} - - ) : ( - // Show empty state with search query info if applicable -
- {searchQuery ? `No agents matching "${searchQuery}"` : "No agents yet"} -
- )} -
-
-
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/MainContent.tsx b/frontend/src/components/dashboard/old/MainContent.tsx deleted file mode 100644 index 053a58a9..00000000 --- a/frontend/src/components/dashboard/old/MainContent.tsx +++ /dev/null @@ -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 ( -
-
-
- {children} -
- - {/* Floating panel rendered here if needed */} - {showFloatingPanel && renderFloatingPanel && renderFloatingPanel()} -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/NavItem.tsx b/frontend/src/components/dashboard/old/NavItem.tsx deleted file mode 100644 index b9f74367..00000000 --- a/frontend/src/components/dashboard/old/NavItem.tsx +++ /dev/null @@ -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 ( - - {(!isCollapsed || !hideIconWhenCollapsed) && ( -
- {icon} -
- )} - {!isCollapsed && {label}} - {isCollapsed && !hideIconWhenCollapsed && ( -
-
- {label} -
-
- )} - - ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/RightSidebar.tsx b/frontend/src/components/dashboard/old/RightSidebar.tsx deleted file mode 100644 index e6cc3d30..00000000 --- a/frontend/src/components/dashboard/old/RightSidebar.tsx +++ /dev/null @@ -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 ( -
-
-

- {panelTitle} -

-
- - -
-
- -
- {panelContent || ( -
-

No content selected

-
- )} -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/dashboard/old/user-account-panel.tsx b/frontend/src/components/dashboard/old/user-account-panel.tsx deleted file mode 100644 index 6338bec9..00000000 --- a/frontend/src/components/dashboard/old/user-account-panel.tsx +++ /dev/null @@ -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 ( -
-
- router.push(account?.personal_account ? `/dashboard` : `/dashboard/${account?.slug}`)} - className="w-10 h-10 rounded-full border-0 justify-center !p-0" - /> -
- - {/* Other UI elements for collapsed mode can remain but we'll keep it minimal */} -
- {/* User dropdown removed from here */} -
-
- ); - } - - return ( -
- {/* Only Account Selector */} - router.push(account?.personal_account ? `/dashboard` : `/dashboard/${account?.slug}`)} - /> - - {/* User dropdown removed */} -
- ); -} \ No newline at end of file diff --git a/frontend/src/components/thread/tool-calls-sidebar.tsx b/frontend/src/components/thread/tool-calls-sidebar.tsx new file mode 100644 index 00000000..cff4835e --- /dev/null +++ b/frontend/src/components/thread/tool-calls-sidebar.tsx @@ -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 ; + if (type.includes('command')) return ; + if (type.includes('search')) return ; + if (type.includes('browser')) return ; + return ; +}; + +const ToolStatus = ({ status }: { status: string }) => { + return ( + + {status === 'running' && ( +
+ )} + {status === 'completed' ? 'Done' : status === 'running' ? 'Running' : 'Failed'} + + ); +}; + +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(); + + 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 ( +
+
+ + {toolCall.tagName} +
+ +
+
+ {toolCall.content} +
+ {toolResult && ( + <> +
+ + Result +
+
+ {toolResult.content} +
+ + )} +
+ ); + } + + return ( +
+
+
+
+ + {toolCall.tagName} +
+ +
+
+ {toolCall.content} +
+
+ + {toolResult && ( +
+
+ + Result +
+
+ {toolResult.content} +
+
+ )} +
+ ); + }; + + return ( + + {showPanel && ( + + {/* Toggle button */} + + + {/* Header */} +
+
+

Tool Calls

+ + {currentToolIndex + 1}/{combinedToolCalls.length} + +
+
+ + +
+
+ + {/* Content */} + +
+ {viewMode === 'single' ? ( +
+ {renderToolCall(combinedToolCalls[currentToolIndex], 'detailed')} + + {/* Navigation */} + {combinedToolCalls.length > 1 && ( +
+ + +
+ )} +
+ ) : ( +
+ {combinedToolCalls.map((toolCall, index) => ( + { + setCurrentToolIndex(index); + setViewMode('single'); + }} + > + {renderToolCall(toolCall, 'compact')} + + ))} +
+ )} +
+
+ + {/* Progress bar */} + {viewMode === 'single' && combinedToolCalls.length > 1 && ( +
+
+ setCurrentToolIndex(parseInt(e.target.value))} + className="flex-1" + /> +
+
+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index f000e3ef..02054139 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -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, - VariantProps {} +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" -function Badge({ className, variant, ...props }: BadgeProps) { return ( -
+ ) } diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..8e4fa13f --- /dev/null +++ b/frontend/src/components/ui/scroll-area.tsx @@ -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) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar }