-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/1957 ai left panel #1970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/1957 ai left panel #1970
Changes from all commits
ba74ed5
23e28b1
894aabb
7d18130
d60f45c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }, | ||
| [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> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use client'; | ||
|
|
||
| import { PanelLeftClose } from 'lucide-react'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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"
doneRepository: hypha-dao/hypha-web Length of output: 2299 Add This file directly imports from 🤖 Prompt for AI Agents |
||
|
|
||
| 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> | ||
| ); | ||
| } | ||
| 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'; |
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: hypha-dao/hypha-web
Length of output: 117
🏁 Script executed:
Repository: hypha-dao/hypha-web
Length of output: 2739
🏁 Script executed:
Repository: hypha-dao/hypha-web
Length of output: 1298
Drag listener cleanup lacks robustness for missed
mouseupevents.If the user releases the mouse outside the window or the tab loses focus mid-drag,
mouseupmay not fire on that window. The listeners andisDraggingstate would persist, and subsequent interactions could start in an inconsistent state. Protect against this by using{ once: true }on one-shot listeners and adding ablurevent 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