mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Wizard: support Local vendors
This commit is contained in:
@@ -91,7 +91,7 @@ function ModelsConfiguratorModal(props: {
|
||||
// start button
|
||||
const startButton = React.useMemo(() => {
|
||||
if (showWizard)
|
||||
return <Button variant='outlined' color='neutral' onClick={handleShowAdvanced} sx={{ backgroundColor: 'background.popup' }}>{isMobile ? 'Advanced Setup' : 'More Services'}</Button>;
|
||||
return <Button variant='outlined' color='neutral' onClick={handleShowAdvanced} sx={{ backgroundColor: 'background.popup' }}>{isMobile ? 'More Services' : 'More Services'}</Button>;
|
||||
// return <Badge size='sm' badgeContent='14 Services' color='neutral' variant='outlined'><Button variant='outlined' color='neutral' onClick={handleShowAdvanced}>{isMobile ? 'Advanced' : 'Switch to Advanced'}</Button></Badge>;
|
||||
if (!isMultiServices)
|
||||
return <Button variant='outlined' color='neutral' onClick={handleShowWizard} sx={{ backgroundColor: 'background.popup' }}>{isMobile ? 'Quick Setup' : 'Quick Setup'}</Button>;
|
||||
|
||||
@@ -225,7 +225,7 @@ export function ModelsServiceSelector(props: {
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip open={noServices && !vendorsMenuAnchor && !isMobile} variant='outlined' color='primary' placement='top' arrow title='Add your first AI service'>
|
||||
<Tooltip open={noServices && !vendorsMenuAnchor && !isMobile} variant='outlined' color='neutral' placement='top' arrow title='Add your first AI service'>
|
||||
<Button variant={noServices ? 'solid' : 'outlined'} onClick={handleShowVendors} disabled={!!vendorsMenuAnchor} startDecorator={<AddIcon />} sx={{ borderColor: 'neutral.outlinedBorder' }}>
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Avatar, Badge, Box, Button, CircularProgress, Input, Sheet, Typography } from '@mui/joy';
|
||||
import { Avatar, Badge, Box, Button, Chip, CircularProgress, Input, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { llmsStoreState, useModelsStore } from '~/common/stores/llms/store-llms';
|
||||
@@ -10,18 +10,33 @@ import { useShallowStabilizer } from '~/common/util/hooks/useShallowObject';
|
||||
import type { IModelVendor } from '../vendors/IModelVendor';
|
||||
import { ModelVendorAnthropic } from '../vendors/anthropic/anthropic.vendor';
|
||||
import { ModelVendorGemini } from '../vendors/gemini/gemini.vendor';
|
||||
import { ModelVendorLMStudio } from '../vendors/lmstudio/lmstudio.vendor';
|
||||
import { ModelVendorLocalAI } from '../vendors/localai/localai.vendor';
|
||||
import { ModelVendorOllama } from '../vendors/ollama/ollama.vendor';
|
||||
import { ModelVendorOpenAI } from '../vendors/openai/openai.vendor';
|
||||
import { llmsUpdateModelsForServiceOrThrow } from '../llm.client';
|
||||
|
||||
|
||||
// configuration
|
||||
const WizardVendors = [
|
||||
{ vendor: ModelVendorOpenAI, apiKeyField: 'oaiKey' },
|
||||
{ vendor: ModelVendorAnthropic, apiKeyField: 'anthropicKey' },
|
||||
{ vendor: ModelVendorGemini, apiKeyField: 'geminiKey' },
|
||||
// { vendor: ModelVendorOpenRouter, apiKeyField: 'oaiKey' },
|
||||
const WizardProviders: ReadonlyArray<WizardProvider> = [
|
||||
{ cat: 'popular', vendor: ModelVendorOpenAI, settingsKey: 'oaiKey' } as const,
|
||||
{ cat: 'popular', vendor: ModelVendorAnthropic, settingsKey: 'anthropicKey' } as const,
|
||||
{ cat: 'popular', vendor: ModelVendorGemini, settingsKey: 'geminiKey' } as const,
|
||||
{ cat: 'local', vendor: ModelVendorLocalAI, settingsKey: 'localAIHost' } as const,
|
||||
{ cat: 'local', vendor: ModelVendorOllama, settingsKey: 'ollamaHost' } as const,
|
||||
{ cat: 'local', vendor: ModelVendorLMStudio, settingsKey: 'oaiHost', omit: true } as const,
|
||||
// { vendor: ModelVendorOpenRouter, settingsKey: 'oaiKey' } as const,
|
||||
] as const;
|
||||
|
||||
type VendorCategory = 'popular' | 'local';
|
||||
|
||||
interface WizardProvider {
|
||||
cat: VendorCategory,
|
||||
vendor: IModelVendor<Record<string, any>, Record<string, any>>,
|
||||
settingsKey: string,
|
||||
omit?: boolean,
|
||||
}
|
||||
|
||||
|
||||
const _styles = {
|
||||
|
||||
@@ -43,6 +58,13 @@ const _styles = {
|
||||
gap: 0.25,
|
||||
} as const,
|
||||
|
||||
text1Mobile: {
|
||||
mb: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0.25,
|
||||
} as const,
|
||||
|
||||
text2: {
|
||||
my: 1,
|
||||
ml: 7.25,
|
||||
@@ -50,63 +72,76 @@ const _styles = {
|
||||
fontSize: 'sm',
|
||||
} as const,
|
||||
|
||||
text2Mobile: {
|
||||
mt: 2,
|
||||
color: 'text.tertiary',
|
||||
fontSize: 'sm',
|
||||
} as const,
|
||||
|
||||
} as const;
|
||||
|
||||
|
||||
function WizardProviderSetup(props: {
|
||||
apiKeyField: string,
|
||||
provider: WizardProvider,
|
||||
isFirst: boolean,
|
||||
vendor: IModelVendor<Record<string, any>, Record<string, any>>,
|
||||
isHidden: boolean,
|
||||
}) {
|
||||
|
||||
const { id: vendorId, name: vendorName, Icon: VendorIcon } = props.vendor;
|
||||
const { cat: providerCat, vendor: providerVendor, settingsKey: providerSettingsKey, omit: providerOmit } = props.provider;
|
||||
|
||||
// state
|
||||
const [localKey, setLocalKey] = React.useState<string | null>(null);
|
||||
const [localValue, setLocalValue] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [updateError, setUpdateError] = React.useState<string | null>(null);
|
||||
|
||||
// external state
|
||||
const stabilizeTransportAccess = useShallowStabilizer<Record<string, any>>();
|
||||
const { serviceAPIKey, serviceLLMsCount } = useModelsStore(useShallow(({ llms, sources }) => {
|
||||
const { serviceKeyValue, serviceLLMsCount } = useModelsStore(useShallow(({ llms, sources }) => {
|
||||
|
||||
// find the service | null
|
||||
const vendorService = sources.find(s => s.vId === vendorId) ?? null;
|
||||
const vendorService = sources.find(s => s.vId === providerVendor.id) ?? null;
|
||||
|
||||
// (safe) service-derived properties
|
||||
const serviceLLMsCount = !vendorService ? null : llms.filter(llm => llm.sId === vendorService.id).length;
|
||||
const serviceAccess = stabilizeTransportAccess(props.vendor.getTransportAccess(vendorService?.setup));
|
||||
const serviceAPIKey = !serviceAccess ? null : serviceAccess[props.apiKeyField] ?? null;
|
||||
const serviceAccess = stabilizeTransportAccess(providerVendor.getTransportAccess(vendorService?.setup));
|
||||
const serviceKeyValue = !serviceAccess ? null : vendorService?.setup[providerSettingsKey] ?? null;
|
||||
|
||||
return {
|
||||
serviceAPIKey,
|
||||
serviceKeyValue,
|
||||
serviceLLMsCount,
|
||||
};
|
||||
}));
|
||||
|
||||
// [effect] initialize the local key
|
||||
const triggerValueLoad = localValue === null;
|
||||
React.useEffect(() => {
|
||||
if (localKey === null)
|
||||
setLocalKey(serviceAPIKey || '');
|
||||
}, [localKey, serviceAPIKey]);
|
||||
if (triggerValueLoad)
|
||||
setLocalValue(serviceKeyValue || '');
|
||||
}, [serviceKeyValue, triggerValueLoad]);
|
||||
|
||||
|
||||
// derived
|
||||
const isLocal = providerCat === 'local';
|
||||
const valueName = isLocal ? 'server address' : 'API Key';
|
||||
const { name: vendorName, Icon: VendorIcon } = providerVendor;
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
const handleTextChanged = React.useCallback((e: React.ChangeEvent) => {
|
||||
setLocalKey((e.target as HTMLInputElement).value);
|
||||
setLocalValue((e.target as HTMLInputElement).value);
|
||||
}, []);
|
||||
|
||||
const handleSetServiceKey = React.useCallback(async () => {
|
||||
const handleSetServiceKeyValue = React.useCallback(async () => {
|
||||
|
||||
// create the service if missing
|
||||
const { sources: llmsServices, createModelsService, updateServiceSettings, setLLMs } = llmsStoreState();
|
||||
const vendorService = llmsServices.find(s => s.vId === vendorId) || createModelsService(props.vendor);
|
||||
const vendorService = llmsServices.find(s => s.vId === providerVendor.id) || createModelsService(providerVendor);
|
||||
const vendorServiceId = vendorService.id;
|
||||
|
||||
// set the key
|
||||
const newKey = localKey?.trim() ?? '';
|
||||
updateServiceSettings(vendorServiceId, { [props.apiKeyField]: newKey });
|
||||
const newKey = localValue?.trim() ?? '';
|
||||
updateServiceSettings(vendorServiceId, { [providerSettingsKey]: newKey });
|
||||
|
||||
// if the key is empty, remove the models
|
||||
if (!newKey) {
|
||||
@@ -121,7 +156,7 @@ function WizardProviderSetup(props: {
|
||||
try {
|
||||
await llmsUpdateModelsForServiceOrThrow(vendorService.id, true);
|
||||
} catch (error: any) {
|
||||
let errorText = error.message || 'An error occurred';
|
||||
let errorText = error.message || `An error occurred. Please check your ${valueName}.`;
|
||||
if (errorText.includes('Incorrect API key'))
|
||||
errorText = '[OpenAI issue] Unauthorized: Incorrect API key.';
|
||||
setUpdateError(errorText);
|
||||
@@ -129,12 +164,12 @@ function WizardProviderSetup(props: {
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
||||
}, [localKey, props.apiKeyField, props.vendor, vendorId]);
|
||||
}, [localValue, providerSettingsKey, providerVendor, valueName]);
|
||||
|
||||
|
||||
// memoed components
|
||||
|
||||
const endButtons = React.useMemo(() => ((localKey || '') === (serviceAPIKey || '')) ? null : (
|
||||
const endButtons = React.useMemo(() => ((localValue || '') === (serviceKeyValue || '')) ? null : (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{/*<TooltipOutlined title='Clear Key'>*/}
|
||||
{/* <IconButton variant='outlined' color='neutral' onClick={handleClear}>*/}
|
||||
@@ -144,17 +179,26 @@ function WizardProviderSetup(props: {
|
||||
{/*<TooltipOutlined title='Confirm'>*/}
|
||||
<Button
|
||||
variant='solid' color='primary'
|
||||
onClick={handleSetServiceKey}
|
||||
onClick={handleSetServiceKeyValue}
|
||||
// endDecorator={<CheckRoundedIcon />}
|
||||
>
|
||||
{!serviceAPIKey ? 'Confirm' : !localKey?.trim() ? 'Clear' : 'Update'}
|
||||
{!serviceKeyValue ? 'Confirm' : !localValue?.trim() ? 'Clear' : 'Update'}
|
||||
</Button>
|
||||
{/*</TooltipOutlined>*/}
|
||||
</Box>
|
||||
), [handleSetServiceKey, localKey, serviceAPIKey]);
|
||||
), [handleSetServiceKeyValue, localValue, serviceKeyValue]);
|
||||
|
||||
|
||||
return (
|
||||
// heuristics for warnings
|
||||
const isOnLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost';
|
||||
|
||||
return props.isHidden ? null : providerOmit ? (
|
||||
<Box sx={{ ..._styles.text1, my: 0, minHeight: '2.5rem' /* to mimic the other items */ }}>
|
||||
{!isOnLocalhost && <Typography level='body-xs'>
|
||||
Please make sure the addresses can be reached from "{typeof window !== 'undefined' ? window.location.hostname : 'this server'}". If you are using a local service, you may need to use a public URL.
|
||||
</Typography>}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
@@ -186,13 +230,13 @@ function WizardProviderSetup(props: {
|
||||
{/* Line 2 */}
|
||||
<Input
|
||||
fullWidth
|
||||
name={`wizard-api-key-${vendorId}`}
|
||||
name={`wizard-settings-value-${providerVendor.id}`}
|
||||
autoComplete='off'
|
||||
variant='outlined'
|
||||
value={localKey ?? ''}
|
||||
value={localValue ?? ''}
|
||||
onChange={handleTextChanged}
|
||||
placeholder={`${vendorName} API Key`}
|
||||
type='password'
|
||||
placeholder={`${vendorName} ${valueName}`}
|
||||
type={isLocal ? undefined : 'password'}
|
||||
// error={!isValidKey}
|
||||
// startDecorator={<props.vendorIcon />}
|
||||
endDecorator={endButtons}
|
||||
@@ -206,7 +250,7 @@ function WizardProviderSetup(props: {
|
||||
{/*{!isLoading && !updateError && !!llmsCount && (*/}
|
||||
{/* <Typography level='body-xs' sx={{ ml: 7, px: 0.5 }}>{llmsCount} models added.</Typography>*/}
|
||||
{/*)}*/}
|
||||
{!isLoading && !updateError && !serviceLLMsCount && !!serviceAPIKey && (
|
||||
{!isLoading && !updateError && !serviceLLMsCount && !!serviceKeyValue && (
|
||||
<Typography level='body-xs' color='warning' sx={{ ml: 7, px: 0.5 }}>No models found.</Typography>
|
||||
)}
|
||||
{!!updateError && <Typography level='body-xs' color='danger' sx={{ ml: 7, px: 0.5 }}>{updateError}</Typography>}
|
||||
@@ -223,21 +267,24 @@ export function ModelsWizard(props: {
|
||||
}) {
|
||||
|
||||
// state
|
||||
// const [category, setCategory] = React.useState<'popular' | 'local'>('popular');
|
||||
const [activeCategory, setActiveCategory] = React.useState<VendorCategory>('popular');
|
||||
|
||||
// derived
|
||||
const isLocal = activeCategory === 'local';
|
||||
|
||||
return (
|
||||
<Sheet variant='soft' sx={_styles.container}>
|
||||
|
||||
<Box sx={_styles.text1}>
|
||||
<Typography level='title-sm'>
|
||||
Enter API keys to connect your AI services.
|
||||
{/*<Chip variant='outlined' sx={{ ml: 0.5, mr: 0.25 }} onClick={() => setCategory('popular')}>*/}
|
||||
{/* popular*/}
|
||||
{/*</Chip>*/}
|
||||
{/*<Chip variant='outlined' sx={{ ml: 0.25, mr: 0.5 }} onClick={() => setCategory('popular')}>*/}
|
||||
{/* local*/}
|
||||
{/*</Chip>*/}
|
||||
{/*AI services.*/}
|
||||
<Box sx={props.isMobile ? _styles.text1Mobile : _styles.text1}>
|
||||
<Typography component='div' level='title-sm'>
|
||||
Enter {isLocal ? 'the addresses of ' : 'your API keys for '}
|
||||
<Chip variant={!isLocal ? 'solid' : 'outlined'} sx={{ mx: 0.25 }} onClick={() => setActiveCategory('popular')}>
|
||||
Popular
|
||||
</Chip>
|
||||
<Chip variant={isLocal ? 'solid' : 'outlined'} sx={{ mx: 0.25 }} onClick={() => setActiveCategory('local')}>
|
||||
Local
|
||||
</Chip>
|
||||
{' '}AI services below.
|
||||
</Typography>
|
||||
{/*<Box sx={{ fontSize: 'sm', color: 'text.primary' }}>*/}
|
||||
{/* Enter API keys to connect your AI services.{' '}*/}
|
||||
@@ -245,13 +292,25 @@ export function ModelsWizard(props: {
|
||||
{/*</Box>*/}
|
||||
</Box>
|
||||
|
||||
{WizardVendors.map(({ vendor, apiKeyField }, index) => (
|
||||
<WizardProviderSetup key={vendor.id} apiKeyField={apiKeyField} isFirst={!index} vendor={vendor} />
|
||||
{WizardProviders.map((provider, index) => (
|
||||
<WizardProviderSetup
|
||||
key={provider.vendor.id}
|
||||
provider={provider}
|
||||
isFirst={!index}
|
||||
isHidden={provider.cat !== activeCategory}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Box sx={_styles.text2}>
|
||||
<Box sx={props.isMobile ? _styles.text2Mobile : _styles.text2}>
|
||||
{/*{!props.isMobile && <>Switch to <Box component='a' onClick={props.onSwitchToAdvanced} sx={{ textDecoration: 'underline', cursor: 'pointer' }}>Advanced</Box> to choose between {getModelVendorsCount()} services.</>}{' '}*/}
|
||||
{!props.isMobile && <>Switch to <Box component='a' onClick={props.onSwitchToAdvanced} sx={{ textDecoration: 'underline', cursor: 'pointer' }}>Advanced</Box> for more services,</>}{' '}
|
||||
{!props.isMobile && <>
|
||||
Switch to{' '}
|
||||
<Box component='a' onClick={props.onSwitchToAdvanced} sx={{ textDecoration: 'underline', cursor: 'pointer' }}>advanced configuration</Box>
|
||||
{/*<Chip variant={isLocal ? 'solid' : 'outlined'} sx={{ ml: 0.25 }} onClick={props.onSwitchToAdvanced}>*/}
|
||||
{/* more services*/}
|
||||
{/*</Chip>*/}
|
||||
{' '}for more services,
|
||||
</>}{' '}
|
||||
or <Box component='a' onClick={props.onSkip} sx={{ textDecoration: 'underline', cursor: 'pointer' }}>skip</Box> for now and do it later.
|
||||
</Box>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user