Skip to content
Merged
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
1 change: 1 addition & 0 deletions web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"react-i18next": "^15.5.1",
"react-joyride": "^2.9.3",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.3",
"react-syntax-highlighter": "^15.6.1",
"react-syntax-highlighter-virtualized-renderer": "^1.1.0",
"react-textarea-autosize": "^8.5.9",
Expand Down
54 changes: 54 additions & 0 deletions web-app/src/components/ui/resizable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react'
import { GripVerticalIcon } from 'lucide-react'
import * as ResizablePrimitive from 'react-resizable-panels'

import { cn } from '@/lib/utils'

function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
className
)}
{...props}
/>
)
}

function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}

function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className
)}
{...props}
>
{withHandle && (
<div className="bg-main-view z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-main-view-fg/10 relative left-0.5">
<GripVerticalIcon className="size-2.5 text-main-view-fg" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}

export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
31 changes: 22 additions & 9 deletions web-app/src/containers/DownloadManegement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ export function DownloadManagement() {
getProviders().then(setProviders)
toast.success(t('common:toast.downloadComplete.title'), {
id: 'download-complete',
description: t('common:toast.downloadComplete.description', { modelId: state.modelId }),
description: t('common:toast.downloadComplete.description', {
modelId: state.modelId,
}),
})
},
[removeDownload, removeLocalDownloadingModel, setProviders, t]
Expand Down Expand Up @@ -237,10 +239,14 @@ export function DownloadManagement() {
<PopoverTrigger asChild>
{isLeftPanelOpen ? (
<div className="bg-left-panel-fg/10 hover:bg-left-panel-fg/12 p-2 rounded-md my-1 relative border border-left-panel-fg/10 cursor-pointer text-left">
<div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
{downloadCount}
<div className="text-left-panel-fg/80 font-medium flex gap-2">
<span>{t('downloads')}</span>
<span>
<div className="bg-primary font-bold size-5 rounded-full flex items-center justify-center text-primary-fg">
{downloadCount}
</div>
</span>
</div>
<p className="text-left-panel-fg/80 font-medium">{t('downloads')}</p>
<div className="mt-2 flex items-center justify-between space-x-2">
<Progress value={overallProgress * 100} />
<span className="text-xs font-medium text-left-panel-fg/80 shrink-0">
Expand Down Expand Up @@ -272,7 +278,9 @@ export function DownloadManagement() {
>
<div className="flex flex-col">
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
<p className="text-xs text-main-view-fg/70">{t('downloading')}</p>
<p className="text-xs text-main-view-fg/70">
{t('downloading')}
</p>
</div>
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
{appUpdateState.isDownloading && (
Expand Down Expand Up @@ -309,10 +317,15 @@ export function DownloadManagement() {
title="Cancel download"
onClick={() => {
abortDownload(download.name).then(() => {
toast.info(t('common:toast.downloadCancelled.title'), {
id: 'cancel-download',
description: t('common:toast.downloadCancelled.description'),
})
toast.info(
t('common:toast.downloadCancelled.title'),
{
id: 'cancel-download',
description: t(
'common:toast.downloadCancelled.description'
),
}
)
if (downloadProcesses.length === 0) {
setIsPopoverOpen(false)
}
Expand Down
27 changes: 23 additions & 4 deletions web-app/src/containers/LeftPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ const LeftPanel = () => {
const searchContainerRef = useRef<HTMLDivElement>(null)
const searchContainerMacRef = useRef<HTMLDivElement>(null)

// Determine if we're in a resizable context (large screen with panel open)
const isResizableContext = !isSmallScreen && open

// Use click outside hook for panel with debugging
useClickOutside(
() => {
Expand Down Expand Up @@ -189,9 +192,17 @@ const LeftPanel = () => {
<aside
ref={panelRef}
className={cn(
'w-48 shrink-0 rounded-lg m-1.5 mr-0 text-left-panel-fg overflow-hidden',
'text-left-panel-fg overflow-hidden',
// Resizable context: full height and width, no margins
isResizableContext && 'h-full w-full',
// Small screen context: fixed positioning and styling
isSmallScreen &&
'fixed h-[calc(100%-16px)] bg-main-view z-40 rounded-sm border border-left-panel-fg/10 m-2 px-1',
'fixed h-[calc(100%-16px)] bg-main-view z-40 rounded-sm border border-left-panel-fg/10 m-2 px-1 w-48',
// Default context: original styling
!isResizableContext &&
!isSmallScreen &&
'w-48 shrink-0 rounded-lg m-1.5 mr-0',
// Visibility controls
open
? 'opacity-100 visibility-visible'
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
Expand All @@ -209,7 +220,12 @@ const LeftPanel = () => {
{!IS_MACOS && (
<div
ref={searchContainerRef}
className="relative top-1.5 mb-4 mx-1 mt-1 w-[calc(100%-32px)] z-50"
className={cn(
'relative top-1.5 mb-4 mt-1 z-50',
isResizableContext
? 'mx-2 w-[calc(100%-48px)]'
: 'mx-1 w-[calc(100%-32px)]'
)}
data-ignore-outside-clicks
>
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
Expand Down Expand Up @@ -241,7 +257,10 @@ const LeftPanel = () => {
{IS_MACOS && (
<div
ref={searchContainerMacRef}
className="relative mb-4 mx-1 mt-1"
className={cn(
'relative mb-4 mt-1',
isResizableContext ? 'mx-2' : 'mx-1'
)}
data-ignore-outside-clicks
>
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
Expand Down
9 changes: 7 additions & 2 deletions web-app/src/containers/ThreadList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,13 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
{...attributes}
{...listeners}
onClick={handleClick}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
setOpenDropdown(true)
}}
className={cn(
'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 group/thread-list transition-all',
'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 transition-all',
isDragging ? 'cursor-move' : 'cursor-pointer',
isActive && 'bg-left-panel-fg/10'
)}
Expand All @@ -122,7 +127,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
<DropdownMenuTrigger asChild>
<IconDots
size={14}
className="text-left-panel-fg/60 shrink-0 cursor-pointer px-0.5 -mr-1 data-[state=open]:bg-left-panel-fg/10 rounded group-hover/thread-list:data-[state=closed]:size-5 size-5 data-[state=closed]:size-0"
className="text-left-panel-fg/60 shrink-0 cursor-pointer px-0.5 -mr-1 data-[state=open]:bg-left-panel-fg/10 rounded size-5"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
Expand Down
4 changes: 4 additions & 0 deletions web-app/src/hooks/useLeftPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { localStorageKey } from '@/constants/localStorage'

type LeftPanelStoreState = {
open: boolean
size: number
setLeftPanel: (value: boolean) => void
setLeftPanelSize: (value: number) => void
}

export const useLeftPanel = create<LeftPanelStoreState>()(
persist(
(set) => ({
open: true,
size: 20, // Default size of 20%
setLeftPanel: (value) => set({ open: value }),
setLeftPanelSize: (value) => set({ size: value }),
}),
{
name: localStorageKey.LeftPanel,
Expand Down
93 changes: 80 additions & 13 deletions web-app/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,47 @@ import { cn } from '@/lib/utils'
import ToolApproval from '@/containers/dialogs/ToolApproval'
import { TranslationProvider } from '@/i18n/TranslationContext'
import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog'
import { useSmallScreen } from '@/hooks/useMediaQuery'
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from '@/components/ui/resizable'
import { useCallback } from 'react'

export const Route = createRootRoute({
component: RootLayout,
})

const AppLayout = () => {
const { productAnalyticPrompt } = useAnalytic()
const { open: isLeftPanelOpen } = useLeftPanel()
const {
open: isLeftPanelOpen,
setLeftPanel,
size: leftPanelSize,
setLeftPanelSize,
} = useLeftPanel()
const isSmallScreen = useSmallScreen()

// Minimum width threshold for auto-close (10% of screen width)
const MIN_PANEL_WIDTH_THRESHOLD = 14

// Handle panel size changes
const handlePanelLayout = useCallback(
(sizes: number[]) => {
if (sizes.length > 0) {
const newSize = sizes[0]

// Close panel if resized below minimum threshold
if (newSize < MIN_PANEL_WIDTH_THRESHOLD) {
setLeftPanel(false)
} else {
setLeftPanelSize(newSize)
}
}
},
[setLeftPanelSize, setLeftPanel]
)

return (
<Fragment>
Expand All @@ -37,22 +70,56 @@ const AppLayout = () => {
{/* Fake absolute panel top to enable window drag */}
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
<DialogAppUpdater />
<div className="flex h-full">
{/* left content panel - only show if not logs route */}
<LeftPanel />

{/* Main content panel */}
<div
className={cn(
'h-full flex w-full p-1 ',
isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]'
)}
{/* Use ResizablePanelGroup only on larger screens */}
{!isSmallScreen && isLeftPanelOpen ? (
<ResizablePanelGroup
direction="horizontal"
className="h-full"
onLayout={handlePanelLayout}
>
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
<Outlet />
{/* Left Panel */}
<ResizablePanel
defaultSize={leftPanelSize}
minSize={MIN_PANEL_WIDTH_THRESHOLD}
maxSize={40}
collapsible
>
<div className="h-full p-1">
<LeftPanel />
</div>
</ResizablePanel>

{/* Resize Handle */}
<ResizableHandle withHandle />

{/* Main Content Panel */}
<ResizablePanel defaultSize={100 - leftPanelSize} minSize={60}>
<div className="h-full p-1 pl-0">
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full h-full rounded-lg overflow-hidden">
<Outlet />
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
) : (
<div className="flex h-full">
{/* left content panel - only show if not logs route */}
<LeftPanel />

{/* Main content panel */}
<div
className={cn(
'h-full flex w-full p-1 ',
isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]'
)}
>
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
<Outlet />
</div>
</div>
</div>
</div>
)}
</main>
{productAnalyticPrompt && <PromptAnalytic />}
</Fragment>
Expand Down
Loading