From 296a68e8d18588fe8e52d4a7b4631cf7a8903a12 Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Mon, 31 Mar 2025 22:47:32 -0700 Subject: [PATCH] sidebar hover states --- .../projects/[id]/threads/[threadId]/page.tsx | 53 +++- frontend/src/components/app-sidebar.tsx | 278 ++++++++++++------ frontend/src/components/nav-user.tsx | 6 +- frontend/src/lib/api.ts | 54 +--- 4 files changed, 258 insertions(+), 133 deletions(-) diff --git a/frontend/src/app/(dashboard)/projects/[id]/threads/[threadId]/page.tsx b/frontend/src/app/(dashboard)/projects/[id]/threads/[threadId]/page.tsx index 0bebbbd3..09085de8 100644 --- a/frontend/src/app/(dashboard)/projects/[id]/threads/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/projects/[id]/threads/[threadId]/page.tsx @@ -16,6 +16,9 @@ type ThreadParams = { id: string; threadId: string }; interface ApiMessage { role: string; content: string; + type?: 'content' | 'tool_call'; + name?: string; + arguments?: string; } interface ApiAgentRun { @@ -71,14 +74,37 @@ export default function ThreadPage({ params }: { params: Promise } // Start streaming the agent's responses with improved implementation const cleanup = streamAgent(runId, { - onMessage: (content: string) => { - // Skip empty content chunks - if (!content.trim()) return; - - // Improved stream update with requestAnimationFrame for smoother UI updates - window.requestAnimationFrame(() => { - setStreamContent(prev => prev + content); - }); + onMessage: (rawData: string) => { + try { + // Parse the outer data structure + const data = JSON.parse(rawData); + + // Handle the nested data structure + if (data.content?.startsWith('data: ')) { + try { + const innerJson = data.content.replace('data: ', ''); + const innerData = JSON.parse(innerJson); + + // Skip empty messages + if (!innerData.content && !innerData.arguments) return; + + window.requestAnimationFrame(() => { + if (innerData.type === 'tool_call') { + const toolContent = innerData.name + ? `Tool: ${innerData.name}\n${innerData.arguments || ''}` + : innerData.arguments || ''; + setStreamContent(prev => prev + (prev ? '\n' : '') + toolContent); + } else if (innerData.type === 'content' && innerData.content) { + setStreamContent(prev => prev + innerData.content); + } + }); + } catch (innerError) { + console.warn('[PAGE] Failed to parse inner data:', innerError); + } + } + } catch (error) { + console.warn('[PAGE] Failed to parse message:', error); + } }, onToolCall: (name: string, args: Record) => { console.log('[PAGE] Tool call received:', name, args); @@ -564,7 +590,16 @@ export default function ThreadPage({ params }: { params: Promise } : 'bg-muted' }`} > -
{message.content}
+
+ {message.type === 'tool_call' ? ( +
+
Tool: {message.name}
+
{message.arguments}
+
+ ) : ( + message.content + )} +
))} diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 95d38674..634ff186 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -164,11 +164,55 @@ export function AppSidebar({ ...props }: React.ComponentProps) { } } + const sidebarLogoMotion = { + tap: { scale: 0.97 }, + hover: { + scale: 1.02, + transition: { type: "spring", stiffness: 400, damping: 17 } + } + } + + const projectItemVariants = { + initial: { opacity: 0, y: -5 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -5 }, + transition: { duration: 0.2 } + } + + const threadListVariants = { + hidden: { opacity: 0, height: 0 }, + visible: { + opacity: 1, + height: "auto", + transition: { + height: { + type: "spring", + stiffness: 500, + damping: 30 + }, + opacity: { duration: 0.2, delay: 0.05 } + } + }, + exit: { + opacity: 0, + height: 0, + transition: { + height: { duration: 0.2 }, + opacity: { duration: 0.1 } + } + } + } + return ( - -
+ + ) { strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className="mr-2.5 h-5 w-5 text-black transition-transform duration-200 group-hover/logo:scale-110" + className="mr-2.5 h-5 w-5 text-black transition-colors group-hover:text-zinc-800" > - AgentPress -
+ AgentPress +
@@ -191,18 +235,24 @@ export function AppSidebar({ ...props }: React.ComponentProps) {

Projects

- + - + + - +

New Project

@@ -223,93 +273,149 @@ export function AppSidebar({ ...props }: React.ComponentProps) { ) : ( projects.map((project) => ( - +
  • -
    - - {project.name} - - -
    + + +
  • {expandedProjectId === project.id && ( -
      +
        {threads[project.id]?.map((thread) => ( -
      • + - {thread.messages[0]?.content || 'New Conversation'} - {thread.messages[0]?.content && thread.messages[0].content.length > 30 ? '...' : ''} + + {thread.messages[0]?.content || 'New Conversation'} + -
      • + ))} -
      • - -
      • + + +
      )} - + )) )}
    @@ -317,11 +423,17 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {user && ( - + + + )} @@ -80,7 +80,9 @@ export function NavUser({ {user.email}
    - +
    + +
    100 ? '...' : ''}`); - const data = JSON.parse(rawData); + // Pass the raw data directly to onMessage for handling in the component + callbacks.onMessage(rawData); - if (data.type === 'content' && data.content) { - if (data.content.startsWith('data: {')) { - try { - const innerData = JSON.parse(data.content.substring(6)); - if (innerData.type === 'content' && innerData.content) { - callbacks.onMessage(innerData.content); - } else if (innerData.type === 'tool_call') { - callbacks.onToolCall(innerData.name, innerData.arguments); - } - } catch { - callbacks.onMessage(data.content); - } - } else { - callbacks.onMessage(data.content); - } - } else if (data.type === 'tool_call') { - callbacks.onToolCall(data.name, data.arguments); - } else if (data.type === 'error') { - console.error(`[STREAM] Error from server: ${data.message}`); - callbacks.onError(data.message instanceof Error ? data.message : new Error(data.message)); - } else if (data.type === 'status') { - console.log(`[STREAM] Status update: ${data.status}`); - - if (data.status === 'completed') { - console.log(`[STREAM] Agent run completed - closing stream for ${agentRunId}`); + // Try to parse for tool calls + try { + const data = JSON.parse(rawData); + if (data.content?.startsWith('data: ')) { + const innerJson = data.content.replace('data: ', ''); + const innerData = JSON.parse(innerJson); - // Close connection first before handling completion - if (eventSourceInstance) { - console.log(`[STREAM] Closing EventSource for ${agentRunId}`); - eventSourceInstance.close(); - eventSourceInstance = null; - } - - // Then notify completion (once) - if (!isClosing) { - console.log(`[STREAM] Calling onClose for ${agentRunId}`); - isClosing = true; - callbacks.onClose(); + if (innerData.type === 'tool_call') { + callbacks.onToolCall(innerData.name || '', innerData.arguments || ''); } } + } catch (parseError) { + // Ignore parsing errors for tool calls + console.debug('[STREAM] Could not parse tool call data:', parseError); } + } catch (error) { - console.error(`[STREAM] Error parsing message:`, error); + console.error(`[STREAM] Error handling message:`, error); callbacks.onError(error instanceof Error ? error : String(error)); } };