import { useState, useEffect, useCallback, useMemo } from 'react'; import { Text, Box, Anchor, rem } from '@mantine/core'; import { Monitor } from 'lucide-react'; import { usePreferencesStore } from '@/stores/preferences'; import type { FrontendPreference } from '@/types'; import { FRONTENDS } from '@/constants'; import { Select } from '@/components/Select'; interface FrontendRequirement { id: string; name: string; url: string; } interface FrontendConfig { value: string; label: string; badges: string[]; requirements?: FrontendRequirement[]; requirementCheck?: () => Promise; } interface FrontendInterfaceSelectorProps { isOnInterfaceScreen?: boolean; } export const FrontendInterfaceSelector = ({ isOnInterfaceScreen = false, }: FrontendInterfaceSelectorProps) => { const { frontendPreference, setFrontendPreference } = usePreferencesStore(); const [frontendRequirements, setFrontendRequirements] = useState< Map >(new Map()); const frontendConfigs: FrontendConfig[] = useMemo( () => [ { value: 'koboldcpp', label: 'Built-in', badges: ['Text', 'Image'], }, { value: 'sillytavern', label: FRONTENDS.SILLYTAVERN, badges: ['Text', 'Image'], requirements: [ { id: 'nodejs', name: 'Node.js', url: 'https://nodejs.org/', }, ], requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(), }, { value: 'openwebui', label: FRONTENDS.OPENWEBUI, badges: ['Text', 'Image'], requirements: [ { id: 'uv', name: 'uv', url: 'https://docs.astral.sh/uv/getting-started/installation/', }, ], requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(), }, { value: 'comfyui', label: FRONTENDS.COMFYUI, badges: ['Image'], requirements: [ { id: 'uv', name: 'uv', url: 'https://docs.astral.sh/uv/getting-started/installation/', }, ], requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(), }, ], [] ); const checkAllFrontendRequirements = useCallback(async () => { const requirementResults = new Map(); for (const config of frontendConfigs) { if (config.requirementCheck) { const isAvailable = await config.requirementCheck(); requirementResults.set(config.value, isAvailable); } else { requirementResults.set(config.value, true); } } setFrontendRequirements(requirementResults); const currentFrontendConfig = frontendConfigs.find( (config) => config.value === frontendPreference ); if (currentFrontendConfig && !requirementResults.get(frontendPreference)) { setFrontendPreference('koboldcpp'); } }, [frontendConfigs, frontendPreference, setFrontendPreference]); const getSelectedFrontendConfig = () => frontendConfigs.find((config) => config.value === frontendPreference); const getUnmetRequirements = () => { const selectedConfig = getSelectedFrontendConfig(); if (!selectedConfig || !selectedConfig.requirements) return []; const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true; return isAvailable ? [] : selectedConfig.requirements; }; const getUnmetRequirementsForFrontend = (frontendValue: string) => { const config = frontendConfigs.find((c) => c.value === frontendValue); if (!config || !config.requirements) return []; const isAvailable = frontendRequirements.get(frontendValue) ?? true; return isAvailable ? [] : config.requirements; }; const isFrontendAvailable = (frontendValue: string) => frontendRequirements.get(frontendValue) ?? true; const handleFrontendPreferenceChange = (value: string | null) => { setFrontendPreference(value as FrontendPreference); }; const renderDisabledFrontendWarnings = () => { const disabledFrontends = frontendConfigs.filter( (config) => !isFrontendAvailable(config.value) ); if (disabledFrontends.length === 0) { return null; } const requirementGroups = new Map(); disabledFrontends.forEach((config) => { const unmetReqs = getUnmetRequirementsForFrontend(config.value); const reqKey = unmetReqs.map((req) => req.id).join(','); if (!requirementGroups.has(reqKey)) { requirementGroups.set(reqKey, []); } requirementGroups.get(reqKey)!.push(config.label); }); return ( {Array.from(requirementGroups.entries()).map( ([reqKey, frontendLabels]) => { const firstDisabledFrontend = disabledFrontends.find( (config) => getUnmetRequirementsForFrontend(config.value) .map((req) => req.id) .join(',') === reqKey ); const unmetReqs = firstDisabledFrontend ? getUnmetRequirementsForFrontend(firstDisabledFrontend.value) : []; const frontendText = frontendLabels.length === 1 ? frontendLabels[0] : frontendLabels.length === 2 ? `${frontendLabels[0]} and ${frontendLabels[1]}` : `${frontendLabels.slice(0, -1).join(', ')}, and ${frontendLabels[frontendLabels.length - 1]}`; const isAre = frontendLabels.length === 1 ? 'is' : 'are'; return ( {frontendText} {isAre} disabled - requires{' '} {unmetReqs.map((req, index) => ( {req.name} {index < unmetReqs.length - 1 ? ', ' : ''} ))} ); } )} ); }; useEffect(() => { const initialize = async () => { await checkAllFrontendRequirements(); }; initialize(); const handleFocus = () => { checkAllFrontendRequirements(); }; window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [checkAllFrontendRequirements]); useEffect(() => { if (frontendPreference) { // eslint-disable-next-line react-hooks/set-state-in-effect checkAllFrontendRequirements(); } }, [frontendPreference, checkAllFrontendRequirements]); return ( <> Frontend Interface {getUnmetRequirements().length === 0 && ( Choose which frontend interface to use for interacting with AI models )} {getUnmetRequirements().length > 0 && ( {getSelectedFrontendConfig()?.label} requires{' '} {getUnmetRequirements().map((req, index) => ( {req.name} {index < getUnmetRequirements().length - 1 ? ', ' : ''} ))}{' '} to be installed on your system )}