/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { ChevronLeft, ChevronRight, Loader2, Plus, Search } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useIsMobile } from '@/hooks/use-mobile' import { Button } from '@/components/ui/button' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty' import { Input } from '@/components/ui/input' import { Dialog } from '@/components/dialog' import { StatusBadge } from '@/components/status-badge' import { getMissingModels } from '../../api' import { DEFAULT_PAGE_SIZE } from '../../constants' import { modelsQueryKeys } from '../../lib' import type { Model } from '../../types' import { useModels } from '../models-provider' type MissingModelsDialogProps = { open: boolean onOpenChange: (open: boolean) => void } export function MissingModelsDialog({ open, onOpenChange, }: MissingModelsDialogProps) { const { t } = useTranslation() const { setOpen, setCurrentRow } = useModels() const isMobile = useIsMobile() const [searchTerm, setSearchTerm] = useState('') const [currentPage, setCurrentPage] = useState(1) const { data, isLoading } = useQuery({ queryKey: modelsQueryKeys.missing(), queryFn: getMissingModels, enabled: open, }) const missingModels = useMemo(() => data?.data || [], [data?.data]) const pageSize = DEFAULT_PAGE_SIZE const handleConfigureModel = (modelName: string) => { setCurrentRow({ model_name: modelName } as unknown as Model) setOpen('create-model') } useEffect(() => { if (open) { // eslint-disable-next-line react-hooks/set-state-in-effect setSearchTerm('') setCurrentPage(1) } }, [open]) const filteredModels = useMemo(() => { if (!searchTerm.trim()) { return missingModels } const keyword = searchTerm.toLowerCase().trim() return missingModels.filter((modelName) => modelName.toLowerCase().includes(keyword) ) }, [missingModels, searchTerm]) const totalItems = filteredModels.length const totalPages = totalItems === 0 ? 1 : Math.ceil(totalItems / Math.max(1, pageSize)) useEffect(() => { if (currentPage > totalPages) { // eslint-disable-next-line react-hooks/set-state-in-effect setCurrentPage(Math.max(1, totalPages)) } }, [currentPage, totalPages]) const paginatedModels = useMemo(() => { const startIndex = (currentPage - 1) * pageSize const endIndex = startIndex + pageSize return filteredModels.slice(startIndex, endIndex) }, [filteredModels, currentPage, pageSize]) const displayStart = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1 const displayEnd = totalItems === 0 ? 0 : Math.min(currentPage * pageSize, totalItems) const showPagination = totalItems > pageSize return ( {isLoading ? (
) : missingModels.length === 0 ? (

{t('No missing models found.')}

{t('All models in use are properly configured.')}

) : (
{t('Showing')} {displayStart}-{displayEnd} {t('of')} {totalItems}
{ setSearchTerm(event.target.value) setCurrentPage(1) }} placeholder={t('Search models...')} className='pl-9' aria-label={t('Search missing models')} />
{filteredModels.length === 0 ? ( {t('No matches found')} {t('Try adjusting your search to locate a missing model.')} ) : (
{paginatedModels.map((modelName) => (
))}
{t('Page {{current}} of {{total}}', { current: currentPage, total: totalPages, })}
{showPagination && (
)}
)}
)}
) }