mirror of https://github.com/kortix-ai/suna.git
sidebar hover states
This commit is contained in:
parent
c4cb7bc920
commit
296a68e8d1
|
@ -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<ThreadParams> }
|
|||
|
||||
// Start streaming the agent's responses with improved implementation
|
||||
const cleanup = streamAgent(runId, {
|
||||
onMessage: (content: string) => {
|
||||
// Skip empty content chunks
|
||||
if (!content.trim()) return;
|
||||
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;
|
||||
|
||||
// Improved stream update with requestAnimationFrame for smoother UI updates
|
||||
window.requestAnimationFrame(() => {
|
||||
setStreamContent(prev => prev + content);
|
||||
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<string, unknown>) => {
|
||||
console.log('[PAGE] Tool call received:', name, args);
|
||||
|
@ -564,7 +590,16 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap break-words">{message.content}</div>
|
||||
<div className="whitespace-pre-wrap break-words">
|
||||
{message.type === 'tool_call' ? (
|
||||
<div className="font-mono text-xs">
|
||||
<div className="text-muted-foreground">Tool: {message.name}</div>
|
||||
<div className="mt-1">{message.arguments}</div>
|
||||
</div>
|
||||
) : (
|
||||
message.content
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -164,11 +164,55 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Sidebar collapsible="offcanvas" {...props}>
|
||||
<SidebarHeader className="border-b-0 h-14 px-4 py-3">
|
||||
<Link href="/dashboard" className="flex items-center">
|
||||
<div className="group/logo flex items-center">
|
||||
<Link href="/dashboard" className="flex items-center group">
|
||||
<motion.div
|
||||
className="flex items-center"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={sidebarLogoMotion}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -177,12 +221,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
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"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
<span className="text-lg font-medium tracking-tight">AgentPress</span>
|
||||
</div>
|
||||
<span className="text-lg font-medium tracking-tight group-hover:text-zinc-800">AgentPress</span>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="py-0 px-2">
|
||||
|
@ -191,18 +235,24 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
<div className="flex items-center justify-between px-2 py-1.5">
|
||||
<h3 className="text-xs uppercase tracking-wider text-zinc-500 font-medium">Projects</h3>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 rounded-md cursor-pointer transition-colors duration-200"
|
||||
className="h-6 w-6 text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 rounded-md cursor-pointer transition-all duration-150"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
<IconPlus className="size-3.5" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent side="right" className="text-xs">
|
||||
<p>New Project</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
@ -223,33 +273,67 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
</li>
|
||||
) : (
|
||||
projects.map((project) => (
|
||||
<React.Fragment key={project.id}>
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={projectItemVariants}
|
||||
>
|
||||
<li className="relative">
|
||||
<div
|
||||
className={`flex items-center justify-between px-2 py-1.5 text-sm rounded-md transition-all duration-200 ${
|
||||
<motion.div
|
||||
className={`flex items-center justify-between px-2 py-1.5 text-sm rounded-md transition-all duration-200 cursor-pointer ${
|
||||
expandedProjectId === project.id
|
||||
? 'text-zinc-900 font-medium bg-zinc-50'
|
||||
? 'text-zinc-900 font-medium bg-zinc-50 hover:bg-zinc-100'
|
||||
: 'text-zinc-700 hover:bg-zinc-50 hover:text-zinc-900'
|
||||
}`}
|
||||
whileHover={{
|
||||
scale: 1.01,
|
||||
x: 1
|
||||
}}
|
||||
transition={{ duration: 0.15 }}
|
||||
style={{
|
||||
backgroundColor: expandedProjectId === project.id ? 'rgb(249 250 251)' : 'transparent'
|
||||
}}
|
||||
initial={false}
|
||||
animate={{
|
||||
backgroundColor: expandedProjectId === project.id
|
||||
? 'rgb(249 250 251)'
|
||||
: 'transparent'
|
||||
}}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
onClick={(e) => {
|
||||
// Either toggle expansion or navigate to project
|
||||
if (expandedProjectId === project.id) {
|
||||
router.push(`/projects/${project.id}`)
|
||||
} else {
|
||||
toggleProjectExpanded(project.id)
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
aria-expanded={expandedProjectId === project.id}
|
||||
>
|
||||
<Link
|
||||
href={`/projects/${project.id}`}
|
||||
className="flex-1 truncate transition-colors duration-200"
|
||||
>
|
||||
<div className="flex-1 truncate transition-colors duration-200">
|
||||
{project.name}
|
||||
</Link>
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-5 w-5 p-0 ml-1 transition-all duration-200 ${
|
||||
className={`h-5 w-5 p-0 ml-1 rounded-full transition-all duration-200 cursor-pointer ${
|
||||
expandedProjectId === project.id
|
||||
? 'text-zinc-900 hover:bg-zinc-100'
|
||||
: 'text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100'
|
||||
? 'text-zinc-900 hover:bg-zinc-200 hover:text-zinc-950'
|
||||
: 'text-zinc-500 hover:text-zinc-900 hover:bg-zinc-200'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation() // Prevent parent's onClick from firing
|
||||
toggleProjectExpanded(project.id)
|
||||
}}
|
||||
aria-label={expandedProjectId === project.id ? "Collapse project" : "Expand project"}
|
||||
>
|
||||
{expandedProjectId === project.id ? (
|
||||
<IconChevronDown className="size-3.5" />
|
||||
|
@ -257,38 +341,59 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
<IconChevronRight className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</li>
|
||||
<AnimatePresence>
|
||||
{expandedProjectId === project.id && (
|
||||
<motion.li
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
variants={threadListVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<ul className="pl-4 space-y-0.5">
|
||||
<ul className="pl-4 space-y-0.5 mt-0.5">
|
||||
{threads[project.id]?.map((thread) => (
|
||||
<li key={thread.thread_id}>
|
||||
<motion.li
|
||||
key={thread.thread_id}
|
||||
whileHover={{
|
||||
x: 2,
|
||||
}}
|
||||
className={`rounded-md ${
|
||||
pathname?.includes(`/threads/${thread.thread_id}`)
|
||||
? 'bg-zinc-50 text-zinc-900 font-medium'
|
||||
: 'text-zinc-700 hover:bg-zinc-50/70 hover:text-zinc-900'
|
||||
}`}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 25
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/projects/${project.id}/threads/${thread.thread_id}`}
|
||||
className={`block px-2 py-1.5 text-sm rounded-md transition-all duration-200 ${
|
||||
pathname?.includes(`/threads/${thread.thread_id}`)
|
||||
? 'text-zinc-900 font-medium bg-zinc-50'
|
||||
: 'text-zinc-700 hover:bg-zinc-50 hover:text-zinc-900'
|
||||
}`}
|
||||
className="block px-2 py-1.5 text-sm rounded-md transition-all duration-200 cursor-pointer w-full"
|
||||
>
|
||||
<span className="block truncate">
|
||||
{thread.messages[0]?.content || 'New Conversation'}
|
||||
{thread.messages[0]?.content && thread.messages[0].content.length > 30 ? '...' : ''}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</motion.li>
|
||||
))}
|
||||
<li>
|
||||
<motion.li
|
||||
whileHover={{ x: 2 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
className="rounded-md hover:bg-zinc-50/70"
|
||||
>
|
||||
<motion.div
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start px-2 py-1.5 text-sm text-zinc-700 hover:text-zinc-900 hover:bg-zinc-50 transition-all duration-200"
|
||||
className="w-full justify-start px-2 py-1.5 text-sm text-zinc-700 hover:text-zinc-900 transition-all duration-200 cursor-pointer rounded-md"
|
||||
onClick={() => handleCreateThread(project.id)}
|
||||
disabled={isCreatingThread[project.id]}
|
||||
>
|
||||
|
@ -304,12 +409,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</li>
|
||||
</motion.div>
|
||||
</motion.li>
|
||||
</ul>
|
||||
</motion.li>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</React.Fragment>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
|
@ -317,11 +423,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
</SidebarContent>
|
||||
<SidebarFooter className="border-t border-border/40">
|
||||
{user && (
|
||||
<motion.div
|
||||
whileHover={{ y: -1 }}
|
||||
transition={{ type: "spring", stiffness: 500 }}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<NavUser user={{
|
||||
name: user.email?.split('@')[0] || 'Guest',
|
||||
email: user.email || '',
|
||||
avatar: '/avatars/user.jpg',
|
||||
}} />
|
||||
</motion.div>
|
||||
)}
|
||||
</SidebarFooter>
|
||||
<CreateProjectDialog
|
||||
|
|
|
@ -66,7 +66,7 @@ export function NavUser({
|
|||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="hover:bg-zinc-50 rounded-md transition-colors w-full py-1"
|
||||
className="hover:bg-zinc-50 rounded-md transition-all duration-200 w-full py-1 group cursor-pointer active:bg-zinc-100"
|
||||
>
|
||||
<Avatar className="h-7 w-7 rounded-full mr-2">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
|
@ -80,7 +80,9 @@ export function NavUser({
|
|||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<IconDotsVertical className="ml-auto size-3.5 text-zinc-400" />
|
||||
<div className="ml-auto p-0.5 rounded-full transition-all duration-200 group-hover:bg-zinc-100 group-active:bg-zinc-200">
|
||||
<IconDotsVertical className="size-3.5 text-zinc-400 transition-all duration-200 group-hover:text-zinc-600 group-active:text-zinc-700" />
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
|
|
@ -372,51 +372,27 @@ export const streamAgent = (agentRunId: string, callbacks: {
|
|||
// Log raw data for debugging
|
||||
console.log(`[STREAM] Received data: ${rawData.substring(0, 100)}${rawData.length > 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 to parse for tool calls
|
||||
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}`);
|
||||
const data = JSON.parse(rawData);
|
||||
if (data.content?.startsWith('data: ')) {
|
||||
const innerJson = data.content.replace('data: ', '');
|
||||
const innerData = JSON.parse(innerJson);
|
||||
|
||||
if (data.status === 'completed') {
|
||||
console.log(`[STREAM] Agent run completed - closing stream for ${agentRunId}`);
|
||||
|
||||
// Close connection first before handling completion
|
||||
if (eventSourceInstance) {
|
||||
console.log(`[STREAM] Closing EventSource for ${agentRunId}`);
|
||||
eventSourceInstance.close();
|
||||
eventSourceInstance = null;
|
||||
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);
|
||||
}
|
||||
|
||||
// Then notify completion (once)
|
||||
if (!isClosing) {
|
||||
console.log(`[STREAM] Calling onClose for ${agentRunId}`);
|
||||
isClosing = true;
|
||||
callbacks.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[STREAM] Error parsing message:`, error);
|
||||
console.error(`[STREAM] Error handling message:`, error);
|
||||
callbacks.onError(error instanceof Error ? error : String(error));
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue