fix: download management ui and double refresh model
This commit is contained in:
parent
3f51c35229
commit
d3fff154d4
@ -400,20 +400,23 @@ export function DownloadManagement() {
|
|||||||
className="text-main-view-fg/70 cursor-pointer"
|
className="text-main-view-fg/70 cursor-pointer"
|
||||||
title="Cancel download"
|
title="Cancel download"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
serviceHub.models().abortDownload(download.name).then(() => {
|
serviceHub
|
||||||
toast.info(
|
.models()
|
||||||
t('common:toast.downloadCancelled.title'),
|
.abortDownload(download.name)
|
||||||
{
|
.then(() => {
|
||||||
id: 'cancel-download',
|
toast.info(
|
||||||
description: t(
|
t('common:toast.downloadCancelled.title'),
|
||||||
'common:toast.downloadCancelled.description'
|
{
|
||||||
),
|
id: 'cancel-download',
|
||||||
|
description: t(
|
||||||
|
'common:toast.downloadCancelled.description'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (downloadProcesses.length === 0) {
|
||||||
|
setIsPopoverOpen(false)
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
if (downloadProcesses.length === 0) {
|
|
||||||
setIsPopoverOpen(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import { toast } from 'sonner'
|
|||||||
import { DownloadManagement } from '@/containers/DownloadManegement'
|
import { DownloadManagement } from '@/containers/DownloadManegement'
|
||||||
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside'
|
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||||
import { useDownloadStore } from '@/hooks/useDownloadStore'
|
|
||||||
import { DeleteAllThreadsDialog } from '@/containers/dialogs'
|
import { DeleteAllThreadsDialog } from '@/containers/dialogs'
|
||||||
|
|
||||||
const mainMenus = [
|
const mainMenus = [
|
||||||
@ -122,7 +122,7 @@ const LeftPanel = () => {
|
|||||||
) {
|
) {
|
||||||
if (currentIsSmallScreen && open) {
|
if (currentIsSmallScreen && open) {
|
||||||
setLeftPanel(false)
|
setLeftPanel(false)
|
||||||
} else if(!open) {
|
} else if (!open) {
|
||||||
setLeftPanel(true)
|
setLeftPanel(true)
|
||||||
}
|
}
|
||||||
prevScreenSizeRef.current = currentIsSmallScreen
|
prevScreenSizeRef.current = currentIsSmallScreen
|
||||||
@ -179,8 +179,6 @@ const LeftPanel = () => {
|
|||||||
}
|
}
|
||||||
}, [isSmallScreen, open])
|
}, [isSmallScreen, open])
|
||||||
|
|
||||||
const { downloads, localDownloadingModels } = useDownloadStore()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Backdrop overlay for small screens */}
|
{/* Backdrop overlay for small screens */}
|
||||||
@ -262,15 +260,8 @@ const LeftPanel = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col justify-between overflow-hidden mt-0 !h-[calc(100%-42px)]">
|
<div className="flex flex-col justify-between overflow-hidden mt-0 !h-[calc(100%-42px)] ">
|
||||||
<div
|
<div className={cn('flex flex-col !h-[calc(100%-200px)]')}>
|
||||||
className={cn(
|
|
||||||
'flex flex-col',
|
|
||||||
Object.keys(downloads).length > 0 || localDownloadingModels.size > 0
|
|
||||||
? 'h-[calc(100%-200px)]'
|
|
||||||
: 'h-[calc(100%-140px)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{IS_MACOS && (
|
{IS_MACOS && (
|
||||||
<div
|
<div
|
||||||
ref={searchContainerMacRef}
|
ref={searchContainerMacRef}
|
||||||
@ -379,7 +370,9 @@ const LeftPanel = () => {
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} />
|
<DeleteAllThreadsDialog
|
||||||
|
onDeleteAll={deleteAllThreads}
|
||||||
|
/>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
@ -468,8 +461,9 @@ const LeftPanel = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<DownloadManagement />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DownloadManagement />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -7,8 +7,15 @@ import { cn } from '@/lib/utils'
|
|||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
|
||||||
// Hook for the dropdown position
|
// Hook for the dropdown position
|
||||||
function useDropdownPosition(open: boolean, containerRef: React.RefObject<HTMLDivElement | null>) {
|
function useDropdownPosition(
|
||||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 })
|
open: boolean,
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>
|
||||||
|
) {
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const updateDropdownPosition = useCallback(() => {
|
const updateDropdownPosition = useCallback(() => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
@ -51,10 +58,18 @@ function useDropdownPosition(open: boolean, containerRef: React.RefObject<HTMLDi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Components for the different sections of the dropdown
|
// Components for the different sections of the dropdown
|
||||||
const ErrorSection = ({ error, t }: { error: string; t: (key: string) => string }) => (
|
const ErrorSection = ({
|
||||||
|
error,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
error: string
|
||||||
|
t: (key: string) => string
|
||||||
|
}) => (
|
||||||
<div className="px-3 py-2 text-sm text-destructive">
|
<div className="px-3 py-2 text-sm text-destructive">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-destructive font-medium">{t('common:failedToLoadModels')}</span>
|
<span className="text-destructive font-medium">
|
||||||
|
{t('common:failedToLoadModels')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-main-view-fg/50 mt-0">{error}</div>
|
<div className="text-xs text-main-view-fg/50 mt-0">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,12 +82,20 @@ const LoadingSection = ({ t }: { t: (key: string) => string }) => (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const EmptySection = ({ inputValue, t }: { inputValue: string; t: (key: string, options?: Record<string, string>) => string }) => (
|
const EmptySection = ({
|
||||||
|
inputValue,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
inputValue: string
|
||||||
|
t: (key: string, options?: Record<string, string>) => string
|
||||||
|
}) => (
|
||||||
<div className="px-3 py-3 text-sm text-main-view-fg/50 text-center">
|
<div className="px-3 py-3 text-sm text-main-view-fg/50 text-center">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{inputValue.trim() ? (
|
{inputValue.trim() ? (
|
||||||
<span className="text-main-view-fg/50">{t('common:noModelsFoundFor', { searchValue: inputValue })}</span>
|
<span className="text-main-view-fg/50">
|
||||||
|
{t('common:noModelsFoundFor', { searchValue: inputValue })}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-main-view-fg/50">{t('common:noModels')}</span>
|
<span className="text-main-view-fg/50">{t('common:noModels')}</span>
|
||||||
)}
|
)}
|
||||||
@ -86,7 +109,7 @@ const ModelsList = ({
|
|||||||
value,
|
value,
|
||||||
highlightedIndex,
|
highlightedIndex,
|
||||||
onModelSelect,
|
onModelSelect,
|
||||||
onHighlight
|
onHighlight,
|
||||||
}: {
|
}: {
|
||||||
filteredModels: string[]
|
filteredModels: string[]
|
||||||
value: string
|
value: string
|
||||||
@ -127,67 +150,95 @@ function useKeyboardNavigation(
|
|||||||
onModelSelect: (model: string) => void,
|
onModelSelect: (model: string) => void,
|
||||||
dropdownRef: React.RefObject<HTMLDivElement | null>
|
dropdownRef: React.RefObject<HTMLDivElement | null>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Scroll to the highlighted element
|
// Scroll to the highlighted element
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (highlightedIndex >= 0 && dropdownRef.current) {
|
if (highlightedIndex >= 0 && dropdownRef.current) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const modelElements = dropdownRef.current?.querySelectorAll('[data-model]')
|
const modelElements =
|
||||||
const highlightedElement = modelElements?.[highlightedIndex] as HTMLElement
|
dropdownRef.current?.querySelectorAll('[data-model]')
|
||||||
|
const highlightedElement = modelElements?.[
|
||||||
|
highlightedIndex
|
||||||
|
] as HTMLElement
|
||||||
if (highlightedElement) {
|
if (highlightedElement) {
|
||||||
highlightedElement.scrollIntoView({
|
highlightedElement.scrollIntoView({
|
||||||
block: 'nearest',
|
block: 'nearest',
|
||||||
behavior: 'auto'
|
behavior: 'auto',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [highlightedIndex, dropdownRef])
|
}, [highlightedIndex, dropdownRef])
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback(
|
||||||
// Open the dropdown with the arrows if closed
|
(e: React.KeyboardEvent) => {
|
||||||
if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
// Open the dropdown with the arrows if closed
|
||||||
if (models.length > 0) {
|
if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||||
e.preventDefault()
|
if (models.length > 0) {
|
||||||
setOpen(true)
|
e.preventDefault()
|
||||||
setHighlightedIndex(0)
|
setOpen(true)
|
||||||
}
|
setHighlightedIndex(0)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!open) return
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault()
|
|
||||||
setHighlightedIndex((prev: number) => filteredModels.length === 0 ? 0 : (prev < filteredModels.length - 1 ? prev + 1 : 0))
|
|
||||||
break
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault()
|
|
||||||
setHighlightedIndex((prev: number) => filteredModels.length === 0 ? 0 : (prev > 0 ? prev - 1 : filteredModels.length - 1))
|
|
||||||
break
|
|
||||||
case 'Enter':
|
|
||||||
e.preventDefault()
|
|
||||||
if (highlightedIndex >= 0 && highlightedIndex < filteredModels.length) {
|
|
||||||
onModelSelect(filteredModels[highlightedIndex])
|
|
||||||
}
|
}
|
||||||
break
|
return
|
||||||
case 'Escape':
|
}
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
if (!open) return
|
||||||
setOpen(false)
|
|
||||||
setHighlightedIndex(-1)
|
switch (e.key) {
|
||||||
break
|
case 'ArrowDown':
|
||||||
case 'PageUp':
|
e.preventDefault()
|
||||||
e.preventDefault()
|
setHighlightedIndex((prev: number) =>
|
||||||
setHighlightedIndex(0)
|
filteredModels.length === 0
|
||||||
break
|
? 0
|
||||||
case 'PageDown':
|
: prev < filteredModels.length - 1
|
||||||
e.preventDefault()
|
? prev + 1
|
||||||
setHighlightedIndex(filteredModels.length - 1)
|
: 0
|
||||||
break
|
)
|
||||||
}
|
break
|
||||||
}, [open, setOpen, models.length, filteredModels, highlightedIndex, setHighlightedIndex, onModelSelect])
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
setHighlightedIndex((prev: number) =>
|
||||||
|
filteredModels.length === 0
|
||||||
|
? 0
|
||||||
|
: prev > 0
|
||||||
|
? prev - 1
|
||||||
|
: filteredModels.length - 1
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault()
|
||||||
|
if (
|
||||||
|
highlightedIndex >= 0 &&
|
||||||
|
highlightedIndex < filteredModels.length
|
||||||
|
) {
|
||||||
|
onModelSelect(filteredModels[highlightedIndex])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
setHighlightedIndex(-1)
|
||||||
|
break
|
||||||
|
case 'PageUp':
|
||||||
|
e.preventDefault()
|
||||||
|
setHighlightedIndex(0)
|
||||||
|
break
|
||||||
|
case 'PageDown':
|
||||||
|
e.preventDefault()
|
||||||
|
setHighlightedIndex(filteredModels.length - 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
models.length,
|
||||||
|
filteredModels,
|
||||||
|
highlightedIndex,
|
||||||
|
setHighlightedIndex,
|
||||||
|
onModelSelect,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return { handleKeyDown }
|
return { handleKeyDown }
|
||||||
}
|
}
|
||||||
@ -266,13 +317,18 @@ export function ModelCombobox({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const events = ['mousedown', 'touchstart']
|
const events = ['mousedown', 'touchstart']
|
||||||
events.forEach(eventType => {
|
events.forEach((eventType) => {
|
||||||
document.addEventListener(eventType, handleClickOutside, { capture: true, passive: true })
|
document.addEventListener(eventType, handleClickOutside, {
|
||||||
|
capture: true,
|
||||||
|
passive: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
events.forEach(eventType => {
|
events.forEach((eventType) => {
|
||||||
document.removeEventListener(eventType, handleClickOutside, { capture: true })
|
document.removeEventListener(eventType, handleClickOutside, {
|
||||||
|
capture: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
@ -286,26 +342,32 @@ export function ModelCombobox({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handler for the input change
|
// Handler for the input change
|
||||||
const handleInputChange = useCallback((newValue: string) => {
|
const handleInputChange = useCallback(
|
||||||
setInputValue(newValue)
|
(newValue: string) => {
|
||||||
onChange(newValue)
|
setInputValue(newValue)
|
||||||
|
onChange(newValue)
|
||||||
|
|
||||||
// Open the dropdown if the user types and there are models
|
// Open the dropdown if the user types and there are models
|
||||||
if (newValue.trim() && models.length > 0) {
|
if (newValue.trim() && models.length > 0) {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
} else {
|
} else {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
}, [onChange, models.length])
|
},
|
||||||
|
[onChange, models.length]
|
||||||
|
)
|
||||||
|
|
||||||
// Handler for the model selection
|
// Handler for the model selection
|
||||||
const handleModelSelect = useCallback((model: string) => {
|
const handleModelSelect = useCallback(
|
||||||
setInputValue(model)
|
(model: string) => {
|
||||||
onChange(model)
|
setInputValue(model)
|
||||||
setOpen(false)
|
onChange(model)
|
||||||
setHighlightedIndex(-1)
|
setOpen(false)
|
||||||
inputRef.current?.focus()
|
setHighlightedIndex(-1)
|
||||||
}, [onChange])
|
inputRef.current?.focus()
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
)
|
||||||
|
|
||||||
// Hook for the keyboard navigation
|
// Hook for the keyboard navigation
|
||||||
const { handleKeyDown } = useKeyboardNavigation(
|
const { handleKeyDown } = useKeyboardNavigation(
|
||||||
@ -376,54 +438,52 @@ export function ModelCombobox({
|
|||||||
onClick={handleDropdownToggle}
|
onClick={handleDropdownToggle}
|
||||||
className="h-6 w-6 p-0 no-underline hover:bg-main-view-fg/10"
|
className="h-6 w-6 p-0 no-underline hover:bg-main-view-fg/10"
|
||||||
>
|
>
|
||||||
{loading ? (
|
<IconChevronDown className="h-3 w-3 opacity-50" />
|
||||||
<IconLoader2 className="h-3 w-3 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<IconChevronDown className="h-3 w-3 opacity-50" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom dropdown rendered as portal */}
|
{/* Custom dropdown rendered as portal */}
|
||||||
{open && dropdownPosition.width > 0 && createPortal(
|
{open &&
|
||||||
<div
|
dropdownPosition.width > 0 &&
|
||||||
ref={dropdownRef}
|
createPortal(
|
||||||
className="fixed z-[9999] bg-main-view border border-main-view-fg/10 rounded-md shadow-lg max-h-[300px] overflow-y-auto text-main-view-fg animate-in fade-in-0 zoom-in-95 duration-200"
|
<div
|
||||||
style={{
|
ref={dropdownRef}
|
||||||
top: dropdownPosition.top,
|
className="fixed z-[9999] bg-main-view border border-main-view-fg/10 rounded-md shadow-lg max-h-[300px] overflow-y-auto text-main-view-fg animate-in fade-in-0 zoom-in-95 duration-200"
|
||||||
left: dropdownPosition.left,
|
style={{
|
||||||
width: dropdownPosition.width,
|
top: dropdownPosition.top,
|
||||||
minWidth: dropdownPosition.width,
|
left: dropdownPosition.left,
|
||||||
maxWidth: dropdownPosition.width,
|
width: dropdownPosition.width,
|
||||||
pointerEvents: 'auto',
|
minWidth: dropdownPosition.width,
|
||||||
}}
|
maxWidth: dropdownPosition.width,
|
||||||
data-dropdown="model-combobox"
|
pointerEvents: 'auto',
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
}}
|
||||||
onWheel={(e) => e.stopPropagation()}
|
data-dropdown="model-combobox"
|
||||||
>
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
{/* Error state */}
|
onWheel={(e) => e.stopPropagation()}
|
||||||
{error && <ErrorSection error={error} t={t} />}
|
>
|
||||||
|
{/* Error state */}
|
||||||
|
{error && <ErrorSection error={error} t={t} />}
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{loading && <LoadingSection t={t} />}
|
{loading && <LoadingSection t={t} />}
|
||||||
|
|
||||||
{/* Models list */}
|
{/* Models list */}
|
||||||
{!loading && !error && (
|
{!loading &&
|
||||||
filteredModels.length === 0 ? (
|
!error &&
|
||||||
<EmptySection inputValue={inputValue} t={t} />
|
(filteredModels.length === 0 ? (
|
||||||
) : (
|
<EmptySection inputValue={inputValue} t={t} />
|
||||||
<ModelsList
|
) : (
|
||||||
filteredModels={filteredModels}
|
<ModelsList
|
||||||
value={value}
|
filteredModels={filteredModels}
|
||||||
highlightedIndex={highlightedIndex}
|
value={value}
|
||||||
onModelSelect={handleModelSelect}
|
highlightedIndex={highlightedIndex}
|
||||||
onHighlight={setHighlightedIndex}
|
onModelSelect={handleModelSelect}
|
||||||
/>
|
onHighlight={setHighlightedIndex}
|
||||||
)
|
/>
|
||||||
)}
|
))}
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user