Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type { Metadata } from 'next';
import { Footer, Html, ThemeProvider } from '@hypha-platform/ui/server';
import { AuthProvider } from '@hypha-platform/authentication';
import { useAuthentication } from '@hypha-platform/authentication';
import { ConnectedButtonProfile } from '@hypha-platform/epics';
import {
AiLeftPanelLayout,
ConnectedButtonProfile,
} from '@hypha-platform/epics';
import { EvmProvider } from '@hypha-platform/evm';
import { useMe } from '@hypha-platform/core/client';
import { fileRouter } from '@hypha-platform/core/server';
Expand Down Expand Up @@ -110,31 +113,35 @@ export default async function RootLayout({
safariWebId={safariWebId}
serviceWorkerPath={serviceWorkerPath}
>
<MenuTop logoHref={ROOT_URL}>
<ConnectedButtonProfile
useAuthentication={useAuthentication}
useMe={useMe}
newUserRedirectPath="/profile/signup"
baseRedirectPath="/my-spaces"
navItems={[
{
label: 'Network',
href: `/${lang}/network`,
},
{
label: 'My Spaces',
href: `/${lang}/my-spaces`,
},
]}
/>
</MenuTop>
<NextSSRPlugin routerConfig={extractRouterConfig(fileRouter)} />
<div className="mb-auto pb-8">
<div className="pt-9 h-full flex justify-normal">
<div className="w-full h-full">{children}</div>
<div className="flex h-screen flex-col overflow-hidden">
<MenuTop logoHref={ROOT_URL}>
<ConnectedButtonProfile
useAuthentication={useAuthentication}
useMe={useMe}
newUserRedirectPath="/profile/signup"
baseRedirectPath="/my-spaces"
navItems={[
{
label: 'Network',
href: `/${lang}/network`,
},
{
label: 'My Spaces',
href: `/${lang}/my-spaces`,
},
]}
/>
</MenuTop>
<NextSSRPlugin routerConfig={extractRouterConfig(fileRouter)} />
<div className="flex min-h-0 flex-1 flex-col overflow-hidden pt-9">
<AiLeftPanelLayout>
<>
<div className="w-full shrink-0 pb-8">{children}</div>
<Footer />
</>
</AiLeftPanelLayout>
</div>
</div>
<Footer />
</NotificationSubscriber>
</EvmProvider>
</ThemeProvider>
Expand Down
128 changes: 128 additions & 0 deletions packages/epics/src/common/ai-left-panel-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use client';

import { useCallback, useRef, useState } from 'react';
import { Bot, ChevronsLeftRight, PanelLeftOpen } from 'lucide-react';

import { AiLeftPanel } from './ai-left-panel';
import { useIsMobile } from '../hooks';
import { Drawer, DrawerContent } from '@hypha-platform/ui';
import { cn } from '@hypha-platform/ui-utils';

const MIN_WIDTH = 240;
const MAX_WIDTH = 600;
const DEFAULT_WIDTH = 320;

type AiLeftPanelLayoutProps = {
children: React.ReactNode;
};

export function AiLeftPanelLayout({ children }: AiLeftPanelLayoutProps) {
const isMobile = useIsMobile();
const [panelOpen, setPanelOpen] = useState(true);
const [panelWidth, setPanelWidth] = useState(DEFAULT_WIDTH);
const [isDragging, setIsDragging] = useState(false);
const dragStartX = useRef(0);
const dragStartWidth = useRef(DEFAULT_WIDTH);

const onResizeMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
dragStartX.current = e.clientX;
dragStartWidth.current = panelWidth;
setIsDragging(true);

const onMouseMove = (e: MouseEvent) => {
const delta = e.clientX - dragStartX.current;
const newWidth = Math.min(
MAX_WIDTH,
Math.max(MIN_WIDTH, dragStartWidth.current + delta),
);
setPanelWidth(newWidth);
};

const onMouseUp = () => {
setIsDragging(false);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};

window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
Comment on lines +34 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

FILE="packages/epics/src/common/ai-left-panel-layout.tsx"
wc -l "$FILE"

Repository: hypha-dao/hypha-web

Length of output: 117


🏁 Script executed:

FILE="packages/epics/src/common/ai-left-panel-layout.tsx"
sed -n '1,80p' "$FILE"

Repository: hypha-dao/hypha-web

Length of output: 2739


🏁 Script executed:

FILE="packages/epics/src/common/ai-left-panel-layout.tsx"
rg -n "addEventListener|removeEventListener|useEffect|useRef|isDragging" "$FILE" -A 2 -B 1

Repository: hypha-dao/hypha-web

Length of output: 1298


Drag listener cleanup lacks robustness for missed mouseup events.

If the user releases the mouse outside the window or the tab loses focus mid-drag, mouseup may not fire on that window. The listeners and isDragging state would persist, and subsequent interactions could start in an inconsistent state. Protect against this by using { once: true } on one-shot listeners and adding a blur event handler as a secondary cleanup trigger.

💡 Hardening listener teardown
       const onMouseUp = () => {
         setIsDragging(false);
         window.removeEventListener('mousemove', onMouseMove);
         window.removeEventListener('mouseup', onMouseUp);
+        window.removeEventListener('blur', onMouseUp);
       };

       window.addEventListener('mousemove', onMouseMove);
-      window.addEventListener('mouseup', onMouseUp);
+      window.addEventListener('mouseup', onMouseUp, { once: true });
+      window.addEventListener('blur', onMouseUp, { once: true });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-left-panel-layout.tsx` around lines 34 - 50, The
drag listeners can remain active if a mouseup is missed; update the drag
teardown in the drag-start logic so mouseup is registered with { once: true }
and add a window 'blur' handler to perform the same cleanup as onMouseUp;
specifically, keep your existing onMouseMove and onMouseUp functions (referenced
by dragStartX, dragStartWidth, setPanelWidth, setIsDragging) but register
window.addEventListener('mouseup', onMouseUp, { once: true }) and also add a
window.addEventListener('blur', onMouseUp) so the blur event will call the same
cleanup removing the mousemove listener and resetting isDragging; ensure the
same onMouseMove and onMouseUp references are used when removing listeners.

},
[panelWidth],
);

return (
<div className="relative flex h-full min-h-0 flex-1 overflow-hidden">
{!isMobile && (
<>
<div
className={cn(
'relative flex h-full flex-shrink-0 flex-col transition-all duration-300',
panelOpen ? '' : 'w-0 overflow-hidden',
)}
style={panelOpen ? { width: panelWidth } : undefined}
>
<AiLeftPanel onClose={() => setPanelOpen(false)} />

{panelOpen && (
<div
role="separator"
aria-orientation="vertical"
aria-valuenow={panelWidth}
onMouseDown={onResizeMouseDown}
className={cn(
'absolute top-0 right-0 z-20 flex h-full w-1 cursor-col-resize items-center justify-center transition-colors',
isDragging
? 'bg-primary'
: 'hover:bg-primary/20 group hover:bg-primary/20',
)}
title="Drag to resize"
>
<div
className={cn(
'flex h-8 w-4 items-center justify-center rounded transition-opacity',
isDragging
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100',
)}
>
<ChevronsLeftRight className="h-3 w-3 text-primary" />
</div>
</div>
)}
</div>
</>
)}

{!panelOpen && (
<button
type="button"
onClick={() => setPanelOpen(true)}
className="fixed left-0 top-[4.5rem] z-30 flex items-center gap-1.5 rounded-r-xl border border-l-0 border-border bg-card px-2 py-1.5 text-xs text-muted-foreground transition-all duration-200 hover:bg-muted hover:text-foreground"
title="Open AI panel"
>
<Bot className="h-3.5 w-3.5 text-primary" />
<PanelLeftOpen className="h-3.5 w-3.5 text-primary" />
</button>
)}

{isMobile && (
<Drawer open={panelOpen} onOpenChange={setPanelOpen} direction="left">
<DrawerContent
className="inset-x-auto right-auto left-0 h-full w-[85vw] max-w-[360px] rounded-r-2xl rounded-t-none border-r"
style={{ top: 0, bottom: 0, marginTop: 0 }}
>
<div className="h-full">
<AiLeftPanel onClose={() => setPanelOpen(false)} />
</div>
</DrawerContent>
</Drawer>
)}

<div className="flex min-h-0 flex-1 flex-col items-start overflow-auto pt-5">
{children}
</div>
</div>
);
}
34 changes: 34 additions & 0 deletions packages/epics/src/common/ai-left-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { PanelLeftClose } from 'lucide-react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd package.json | while read -r manifest; do
  echo "== $manifest =="
  jq -r '"name=\(.name // "unknown") | dep=\(.dependencies["lucide-react"] // "null") | devDep=\(.devDependencies["lucide-react"] // "null") | peerDep=\(.peerDependencies["lucide-react"] // "null")"' "$manifest"
done

Repository: hypha-dao/hypha-web

Length of output: 2299


Add lucide-react to packages/epics dependencies.

This file directly imports from lucide-react, but the package does not explicitly declare it as a dependency. Currently relying on the transitive dependency from the root workspace. Each package should declare its own dependencies to avoid strict-install/build breaks and maintain proper package boundaries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-left-panel.tsx` at line 3, Add "lucide-react" as
a direct dependency of the packages/epics package.json because ai-left-panel.tsx
imports PanelLeftClose from 'lucide-react'; update the dependencies section in
packages/epics/package.json to include the same lucide-react version used by the
workspace (or a compatible semver), then run install/build to verify the import
resolves without relying on transitive root workspace deps.


import { cn } from '@hypha-platform/ui-utils';

type AiLeftPanelProps = {
onClose: () => void;
className?: string;
};

export function AiLeftPanel({ onClose, className }: AiLeftPanelProps) {
return (
<div
className={cn(
'flex h-full flex-col bg-background-2 border-r border-border',
className,
)}
>
<div className="flex flex-shrink-0 items-center justify-end border-b border-border px-4 py-3">
<button
type="button"
onClick={onClose}
className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
title="Close panel"
aria-label="Close panel"
>
<PanelLeftClose className="h-3.5 w-3.5" />
</button>
</div>
<div className="flex-1 overflow-auto" />
</div>
);
}
2 changes: 2 additions & 0 deletions packages/epics/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './ai-left-panel';
export * from './ai-left-panel-layout';
export * from './authenticated-link-button';
export * from './button-back';
export * from './button-close';
Expand Down
1 change: 1 addition & 0 deletions packages/epics/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './use-db-tokens';
export * from './use-is-mobile';
export * from './use-scroll-to-errors';
export * from './use-db-spaces';
export * from './use-resubmit-proposal-data';
23 changes: 23 additions & 0 deletions packages/epics/src/hooks/use-is-mobile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import * as React from 'react';

const MOBILE_BREAKPOINT = 768;

export function useIsMobile(): boolean {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);

React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);

return !!isMobile;
}
5 changes: 3 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-slot": "^1.2.3"
},
"@radix-ui/react-slot": "^1.2.3",
"vaul": "^0.9.9"
},
"exports": {
".": "./src/index.ts",
"./server": "./src/server.ts"
Expand Down
Loading