diff --git a/web-app/src/containers/DownloadManegement.tsx b/web-app/src/containers/DownloadManegement.tsx index f91a943d3..92bb3ee85 100644 --- a/web-app/src/containers/DownloadManegement.tsx +++ b/web-app/src/containers/DownloadManegement.tsx @@ -400,20 +400,23 @@ export function DownloadManagement() { className="text-main-view-fg/70 cursor-pointer" title="Cancel download" onClick={() => { - serviceHub.models().abortDownload(download.name).then(() => { - toast.info( - t('common:toast.downloadCancelled.title'), - { - id: 'cancel-download', - description: t( - 'common:toast.downloadCancelled.description' - ), + serviceHub + .models() + .abortDownload(download.name) + .then(() => { + toast.info( + t('common:toast.downloadCancelled.title'), + { + id: 'cancel-download', + description: t( + 'common:toast.downloadCancelled.description' + ), + } + ) + if (downloadProcesses.length === 0) { + setIsPopoverOpen(false) } - ) - if (downloadProcesses.length === 0) { - setIsPopoverOpen(false) - } - }) + }) }} /> diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index da596dd4a..1ad0ef560 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -35,7 +35,7 @@ import { toast } from 'sonner' import { DownloadManagement } from '@/containers/DownloadManegement' import { useSmallScreen } from '@/hooks/useMediaQuery' import { useClickOutside } from '@/hooks/useClickOutside' -import { useDownloadStore } from '@/hooks/useDownloadStore' + import { DeleteAllThreadsDialog } from '@/containers/dialogs' const mainMenus = [ @@ -122,7 +122,7 @@ const LeftPanel = () => { ) { if (currentIsSmallScreen && open) { setLeftPanel(false) - } else if(!open) { + } else if (!open) { setLeftPanel(true) } prevScreenSizeRef.current = currentIsSmallScreen @@ -179,8 +179,6 @@ const LeftPanel = () => { } }, [isSmallScreen, open]) - const { downloads, localDownloadingModels } = useDownloadStore() - return ( <> {/* Backdrop overlay for small screens */} @@ -262,15 +260,8 @@ const LeftPanel = () => { )} -
-
0 || localDownloadingModels.size > 0 - ? 'h-[calc(100%-200px)]' - : 'h-[calc(100%-140px)]' - )} - > +
+
{IS_MACOS && (
{ - +
@@ -468,8 +461,9 @@ const LeftPanel = () => { ) })} -
+ +
diff --git a/web-app/src/containers/ModelCombobox.tsx b/web-app/src/containers/ModelCombobox.tsx index ea5b3d670..5ed8ed14d 100644 --- a/web-app/src/containers/ModelCombobox.tsx +++ b/web-app/src/containers/ModelCombobox.tsx @@ -7,8 +7,15 @@ import { cn } from '@/lib/utils' import { useTranslation } from '@/i18n/react-i18next-compat' // Hook for the dropdown position -function useDropdownPosition(open: boolean, containerRef: React.RefObject) { - const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }) +function useDropdownPosition( + open: boolean, + containerRef: React.RefObject +) { + const [dropdownPosition, setDropdownPosition] = useState({ + top: 0, + left: 0, + width: 0, + }) const updateDropdownPosition = useCallback(() => { if (containerRef.current) { @@ -51,10 +58,18 @@ function useDropdownPosition(open: boolean, containerRef: React.RefObject string }) => ( +const ErrorSection = ({ + error, + t, +}: { + error: string + t: (key: string) => string +}) => (
- {t('common:failedToLoadModels')} + + {t('common:failedToLoadModels')} +
{error}
@@ -67,12 +82,20 @@ const LoadingSection = ({ t }: { t: (key: string) => string }) => (
) -const EmptySection = ({ inputValue, t }: { inputValue: string; t: (key: string, options?: Record) => string }) => ( +const EmptySection = ({ + inputValue, + t, +}: { + inputValue: string + t: (key: string, options?: Record) => string +}) => (
{inputValue.trim() ? ( - {t('common:noModelsFoundFor', { searchValue: inputValue })} + + {t('common:noModelsFoundFor', { searchValue: inputValue })} + ) : ( {t('common:noModels')} )} @@ -86,7 +109,7 @@ const ModelsList = ({ value, highlightedIndex, onModelSelect, - onHighlight + onHighlight, }: { filteredModels: string[] value: string @@ -127,67 +150,95 @@ function useKeyboardNavigation( onModelSelect: (model: string) => void, dropdownRef: React.RefObject ) { - // Scroll to the highlighted element useEffect(() => { if (highlightedIndex >= 0 && dropdownRef.current) { requestAnimationFrame(() => { - const modelElements = dropdownRef.current?.querySelectorAll('[data-model]') - const highlightedElement = modelElements?.[highlightedIndex] as HTMLElement + const modelElements = + dropdownRef.current?.querySelectorAll('[data-model]') + const highlightedElement = modelElements?.[ + highlightedIndex + ] as HTMLElement if (highlightedElement) { highlightedElement.scrollIntoView({ block: 'nearest', - behavior: 'auto' + behavior: 'auto', }) } }) } }, [highlightedIndex, dropdownRef]) - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - // Open the dropdown with the arrows if closed - if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { - if (models.length > 0) { - e.preventDefault() - 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]) + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + // Open the dropdown with the arrows if closed + if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { + if (models.length > 0) { + e.preventDefault() + setOpen(true) + setHighlightedIndex(0) } - 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 + } + + 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 + 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 } } @@ -266,13 +317,18 @@ export function ModelCombobox({ } const events = ['mousedown', 'touchstart'] - events.forEach(eventType => { - document.addEventListener(eventType, handleClickOutside, { capture: true, passive: true }) + events.forEach((eventType) => { + document.addEventListener(eventType, handleClickOutside, { + capture: true, + passive: true, + }) }) return () => { - events.forEach(eventType => { - document.removeEventListener(eventType, handleClickOutside, { capture: true }) + events.forEach((eventType) => { + document.removeEventListener(eventType, handleClickOutside, { + capture: true, + }) }) } }, [open]) @@ -286,26 +342,32 @@ export function ModelCombobox({ }, []) // Handler for the input change - const handleInputChange = useCallback((newValue: string) => { - setInputValue(newValue) - onChange(newValue) + const handleInputChange = useCallback( + (newValue: string) => { + setInputValue(newValue) + onChange(newValue) - // Open the dropdown if the user types and there are models - if (newValue.trim() && models.length > 0) { - setOpen(true) - } else { - setOpen(false) - } - }, [onChange, models.length]) + // Open the dropdown if the user types and there are models + if (newValue.trim() && models.length > 0) { + setOpen(true) + } else { + setOpen(false) + } + }, + [onChange, models.length] + ) // Handler for the model selection - const handleModelSelect = useCallback((model: string) => { - setInputValue(model) - onChange(model) - setOpen(false) - setHighlightedIndex(-1) - inputRef.current?.focus() - }, [onChange]) + const handleModelSelect = useCallback( + (model: string) => { + setInputValue(model) + onChange(model) + setOpen(false) + setHighlightedIndex(-1) + inputRef.current?.focus() + }, + [onChange] + ) // Hook for the keyboard navigation const { handleKeyDown } = useKeyboardNavigation( @@ -376,54 +438,52 @@ export function ModelCombobox({ onClick={handleDropdownToggle} className="h-6 w-6 p-0 no-underline hover:bg-main-view-fg/10" > - {loading ? ( - - ) : ( - - )} +
{/* Custom dropdown rendered as portal */} - {open && dropdownPosition.width > 0 && createPortal( -
e.stopPropagation()} - onWheel={(e) => e.stopPropagation()} - > - {/* Error state */} - {error && } + {open && + dropdownPosition.width > 0 && + createPortal( +
e.stopPropagation()} + onWheel={(e) => e.stopPropagation()} + > + {/* Error state */} + {error && } - {/* Loading state */} - {loading && } + {/* Loading state */} + {loading && } - {/* Models list */} - {!loading && !error && ( - filteredModels.length === 0 ? ( - - ) : ( - - ) - )} -
, - document.body - )} + {/* Models list */} + {!loading && + !error && + (filteredModels.length === 0 ? ( + + ) : ( + + ))} +
, + document.body + )}
)