import type { BoxProps, ButtonProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { Button, Flex, Icon, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, Portal, Spacer, Text, } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { EMPTY_ARRAY } from 'app/store/constants'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { $onClickGoToModelManager } from 'app/store/nanostores/onClickGoToModelManager'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { Group, PickerContextState } from 'common/components/Picker/Picker'; import { buildGroup, getRegex, isOption, Picker, usePickerContext } from 'common/components/Picker/Picker'; import { useDisclosure } from 'common/hooks/useBoolean'; import { typedMemo } from 'common/util/typedMemo'; import { uniq } from 'es-toolkit/compat'; import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore'; import { BASE_COLOR_MAP } from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage'; import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton'; import { API_BASE_MODELS, MODEL_TYPE_MAP, MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectIsModelsTabDisabled } from 'features/system/store/configSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { filesize } from 'filesize'; import { memo, useCallback, useMemo, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { PiCaretDownBold, PiLinkSimple } from 'react-icons/pi'; import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships'; import type { AnyModelConfig, BaseModelType } from 'services/api/types'; const selectSelectedModelKeys = createMemoizedSelector(selectParamsSlice, selectLoRAsSlice, (params, loras) => { const keys: string[] = []; const main = params.model; const vae = params.vae; const refiner = params.refinerModel; const controlnet = params.controlLora; if (main) { keys.push(main.key); } if (vae) { keys.push(vae.key); } if (refiner) { keys.push(refiner.key); } if (controlnet) { keys.push(controlnet.key); } for (const { model } of loras.loras) { keys.push(model.key); } return uniq(keys); }); type WithStarred = T & { starred?: boolean }; // Type for models with starred field const getOptionId = (modelConfig: WithStarred) => modelConfig.key; const ModelManagerLink = memo((props: ButtonProps) => { const onClickGoToModelManager = useStore($onClickGoToModelManager); const dispatch = useAppDispatch(); const onClick = useCallback(() => { dispatch(setActiveTab('models')); setInstallModelsTabByName('launchpad'); }, [dispatch]); return ( > handleRef={pickerRef} optionsOrGroups={options} getOptionId={getOptionId} onSelect={onSelect} selectedOption={selectedOption} isMatch={isMatch} OptionComponent={PickerOptionComponent} noOptionsFallback={} noMatchesFallback={t('modelManager.noMatchingModels')} NextToSearchBar={} getIsOptionDisabled={getIsOptionDisabled} searchable initialGroupStates={initialGroupStates} /> ); } ); ModelPicker.displayName = 'ModelPicker'; const optionSx: SystemStyleObject = { p: 2, gap: 2, cursor: 'pointer', borderRadius: 'base', '&[data-selected="true"]': { bg: 'base.700', '&[data-active="true"]': { bg: 'base.650', }, }, '&[data-active="true"]': { bg: 'base.750', }, '&[data-disabled="true"]': { cursor: 'not-allowed', opacity: 0.5, }, '&[data-is-compact="true"]': { px: 1, py: 0.5, }, scrollMarginTop: '24px', // magic number, this is the height of the header }; const optionNameSx: SystemStyleObject = { fontSize: 'sm', noOfLines: 1, fontWeight: 'semibold', '&[data-is-compact="true"]': { fontWeight: 'normal', }, }; const PickerOptionComponent = typedMemo( ({ option, ...rest }: { option: WithStarred } & BoxProps) => { const { $compactView } = usePickerContext>(); const compactView = useStore($compactView); return ( {!compactView && option.cover_image && } {option.starred && } {option.name} {option.file_size > 0 && ( {filesize(option.file_size)} )} {option.usage_info && ( {option.usage_info} )} {option.description && !compactView && {option.description}} ); } ); PickerOptionComponent.displayName = 'PickerItemComponent'; const BASE_KEYWORDS: { [key in BaseModelType]?: string[] } = { 'sd-1': ['sd1', 'sd1.4', 'sd1.5', 'sd-1'], 'sd-2': ['sd2', 'sd2.0', 'sd2.1', 'sd-2'], 'sd-3': ['sd3', 'sd3.0', 'sd3.5', 'sd-3'], }; const isMatch = (model: WithStarred, searchTerm: string) => { const regex = getRegex(searchTerm); const bases = BASE_KEYWORDS[model.base] ?? [model.base]; const testString = `${model.name} ${bases.join(' ')} ${model.type} ${model.description ?? ''} ${model.format}`.toLowerCase(); if (testString.includes(searchTerm) || regex.test(testString)) { return true; } return false; };