mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-04 12:13:28 -07:00
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
import { Anchor, Box, Button, Group, rem, Stack, Text } 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<boolean>;
|
|
}
|
|
|
|
interface FrontendInterfaceSelectorProps {
|
|
isOnInterfaceScreen?: boolean;
|
|
}
|
|
|
|
export const FrontendInterfaceSelector = ({
|
|
isOnInterfaceScreen = false,
|
|
}: FrontendInterfaceSelectorProps) => {
|
|
const {
|
|
frontendPreference,
|
|
setFrontendPreference,
|
|
imageGenerationFrontendPreference,
|
|
setImageGenerationFrontendPreference,
|
|
} = usePreferencesStore();
|
|
|
|
const [frontendRequirements, setFrontendRequirements] = useState<Map<string, boolean>>(new Map());
|
|
|
|
const [showClearDataModal, setShowClearDataModal] = useState(false);
|
|
|
|
const frontendConfigs: FrontendConfig[] = useMemo(
|
|
() => [
|
|
{
|
|
value: 'llamacpp',
|
|
label: FRONTENDS.LLAMA_CPP,
|
|
},
|
|
{
|
|
value: 'koboldcpp',
|
|
label: FRONTENDS.KOBOLDAI_LITE,
|
|
},
|
|
{
|
|
value: 'sillytavern',
|
|
label: FRONTENDS.SILLYTAVERN,
|
|
requirements: [
|
|
{
|
|
id: 'nodejs',
|
|
name: 'Node.js',
|
|
url: 'https://nodejs.org/',
|
|
},
|
|
],
|
|
requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(),
|
|
},
|
|
{
|
|
value: 'openwebui',
|
|
label: FRONTENDS.OPENWEBUI,
|
|
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('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<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();
|
|
};
|
|
void initialize();
|
|
|
|
const handleFocus = () => {
|
|
void checkAllFrontendRequirements();
|
|
};
|
|
|
|
window.addEventListener('focus', handleFocus);
|
|
return () => window.removeEventListener('focus', handleFocus);
|
|
}, [checkAllFrontendRequirements]);
|
|
|
|
useEffect(() => {
|
|
if (frontendPreference) {
|
|
void checkAllFrontendRequirements();
|
|
}
|
|
}, [frontendPreference, checkAllFrontendRequirements]);
|
|
|
|
return (
|
|
<>
|
|
<div>
|
|
<Text fw={500} mb="xs">
|
|
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>
|
|
)}
|
|
|
|
<Group gap="xs" align="flex-end">
|
|
<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) }} />}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
|
|
{frontendPreference === 'openwebui' && (
|
|
<Button
|
|
variant="light"
|
|
color="orange"
|
|
onClick={() => setShowClearDataModal(true)}
|
|
disabled={isOnInterfaceScreen}
|
|
>
|
|
Clear Data
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
|
|
{renderDisabledFrontendWarnings()}
|
|
</div>
|
|
|
|
<Modal
|
|
opened={showClearDataModal}
|
|
onClose={() => setShowClearDataModal(false)}
|
|
title="Clear Open WebUI Data?"
|
|
>
|
|
<Stack gap="md">
|
|
<Text size="sm" c="dimmed">
|
|
This will permanently delete all Open WebUI data including:
|
|
</Text>
|
|
<Box component="ul" pl="md">
|
|
<Text component="li" size="sm" c="dimmed">
|
|
Chat history
|
|
</Text>
|
|
<Text component="li" size="sm" c="dimmed">
|
|
User settings
|
|
</Text>
|
|
<Text component="li" size="sm" c="dimmed">
|
|
Database
|
|
</Text>
|
|
</Box>
|
|
|
|
<Group justify="flex-end" gap="sm">
|
|
<Button variant="subtle" onClick={() => setShowClearDataModal(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button color="red" onClick={() => void handleClearOpenWebUIData()}>
|
|
Clear Data
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
<div>
|
|
<Text fw={500} mb="xs">
|
|
Image Generation Frontend
|
|
</Text>
|
|
|
|
<Text size="sm" c="dimmed" mb="md">
|
|
Choose which frontend to use for image generation specifically
|
|
</Text>
|
|
|
|
<Select
|
|
value={imageGenerationFrontendPreference}
|
|
onChange={handleImageGenerationFrontendChange}
|
|
disabled={isOnInterfaceScreen}
|
|
data={[
|
|
{ value: 'match', label: 'Match Frontend' },
|
|
{ value: 'builtin', label: 'Built-in' },
|
|
]}
|
|
leftSection={<Image style={{ width: rem(16), height: rem(16) }} />}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|