import { Anchor, Box, Button, Group, Stack, Text, rem } from '@mantine/core'; import { Image, Monitor } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Modal } from '@/components/Modal'; import { Select } from '@/components/Select'; import { FRONTENDS } from '@/constants'; import { usePreferencesStore } from '@/stores/preferences'; import type { FrontendPreference, ImageGenerationFrontendPreference } from '@/types'; interface FrontendRequirement { id: string; name: string; url: string; } interface FrontendConfig { value: FrontendPreference; label: string; requirements?: FrontendRequirement[]; requirementCheck?: () => Promise; } interface FrontendInterfaceSelectorProps { isOnInterfaceScreen?: boolean; } export const FrontendInterfaceSelector = ({ isOnInterfaceScreen = false, }: FrontendInterfaceSelectorProps) => { const { frontendPreference, setFrontendPreference, imageGenerationFrontendPreference, setImageGenerationFrontendPreference, } = usePreferencesStore(); const [frontendRequirements, setFrontendRequirements] = useState>(new Map()); const [showClearDataModal, setShowClearDataModal] = useState(false); const frontendConfigs: FrontendConfig[] = useMemo( () => [ { label: FRONTENDS.LLAMA_CPP, value: 'llamacpp', }, { label: FRONTENDS.KOBOLDAI_LITE, value: 'koboldcpp', }, { label: FRONTENDS.SILLYTAVERN, requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(), requirements: [ { id: 'nodejs', name: 'Node.js', url: 'https://nodejs.org/', }, ], value: 'sillytavern', }, { label: FRONTENDS.OPENWEBUI, requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(), requirements: [ { id: 'uv', name: 'uv', url: 'https://docs.astral.sh/uv/getting-started/installation/', }, ], value: 'openwebui', }, ], [], ); 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('llamacpp'); } }, [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 handleImageGenerationFrontendChange = (value: string | null) => { setImageGenerationFrontendPreference(value as ImageGenerationFrontendPreference); }; const handleClearOpenWebUIData = async () => { await window.electronAPI.dependencies.clearOpenWebUIData(); setShowClearDataModal(false); }; 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 ( {[...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(); }; void initialize(); const handleFocus = () => { void checkAllFrontendRequirements(); }; window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [checkAllFrontendRequirements]); useEffect(() => { if (frontendPreference) { void 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 )} } />
); };