mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
271 lines
8.1 KiB
TypeScript
271 lines
8.1 KiB
TypeScript
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<boolean>;
|
|
}
|
|
|
|
interface FrontendInterfaceSelectorProps {
|
|
isOnInterfaceScreen?: boolean;
|
|
}
|
|
|
|
export const FrontendInterfaceSelector = ({
|
|
isOnInterfaceScreen = false,
|
|
}: FrontendInterfaceSelectorProps) => {
|
|
const { frontendPreference, setFrontendPreference } = usePreferencesStore();
|
|
|
|
const [frontendRequirements, setFrontendRequirements] = useState<
|
|
Map<string, boolean>
|
|
>(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<string, boolean>();
|
|
|
|
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<string, string[]>();
|
|
|
|
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 (
|
|
<Box mt="sm">
|
|
{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 (
|
|
<Text key={reqKey} size="sm" c="orange">
|
|
{frontendText} {isAre} disabled - requires{' '}
|
|
{unmetReqs.map((req, index) => (
|
|
<span key={req.id}>
|
|
<Anchor
|
|
href={req.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
td="underline"
|
|
>
|
|
{req.name}
|
|
</Anchor>
|
|
{index < unmetReqs.length - 1 ? ', ' : ''}
|
|
</span>
|
|
))}
|
|
</Text>
|
|
);
|
|
}
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<>
|
|
<Text fw={500} mb="sm">
|
|
Frontend Interface
|
|
</Text>
|
|
|
|
{getUnmetRequirements().length === 0 && (
|
|
<Text size="sm" c="dimmed" mb="md">
|
|
Choose which frontend interface to use for interacting with AI models
|
|
</Text>
|
|
)}
|
|
|
|
{getUnmetRequirements().length > 0 && (
|
|
<Text size="sm" c="red" mb="md">
|
|
{getSelectedFrontendConfig()?.label} requires{' '}
|
|
{getUnmetRequirements().map((req, index) => (
|
|
<span key={req.id}>
|
|
<Anchor
|
|
href={req.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
c="red"
|
|
td="underline"
|
|
>
|
|
{req.name}
|
|
</Anchor>
|
|
{index < getUnmetRequirements().length - 1 ? ', ' : ''}
|
|
</span>
|
|
))}{' '}
|
|
to be installed on your system
|
|
</Text>
|
|
)}
|
|
|
|
<Select
|
|
value={frontendPreference}
|
|
onChange={handleFrontendPreferenceChange}
|
|
disabled={isOnInterfaceScreen}
|
|
data={frontendConfigs.map((config) => ({
|
|
value: config.value,
|
|
label: config.label,
|
|
disabled: !isFrontendAvailable(config.value),
|
|
}))}
|
|
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
|
/>
|
|
|
|
{renderDisabledFrontendWarnings()}
|
|
</>
|
|
);
|
|
};
|