diff --git a/src/modules/llms/models-modal/ModelsModal.tsx b/src/modules/llms/models-modal/ModelsModal.tsx index 2c44f1f99..8a5c8dda0 100644 --- a/src/modules/llms/models-modal/ModelsModal.tsx +++ b/src/modules/llms/models-modal/ModelsModal.tsx @@ -91,7 +91,7 @@ function ModelsConfiguratorModal(props: { // start button const startButton = React.useMemo(() => { if (showWizard) - return ; + return ; // return ; if (!isMultiServices) return ; diff --git a/src/modules/llms/models-modal/ModelsServiceSelector.tsx b/src/modules/llms/models-modal/ModelsServiceSelector.tsx index 4f2496017..53360dbf9 100644 --- a/src/modules/llms/models-modal/ModelsServiceSelector.tsx +++ b/src/modules/llms/models-modal/ModelsServiceSelector.tsx @@ -225,7 +225,7 @@ export function ModelsServiceSelector(props: { ) : ( - + diff --git a/src/modules/llms/models-modal/ModelsWizard.tsx b/src/modules/llms/models-modal/ModelsWizard.tsx index d60377b37..c1d97bed5 100644 --- a/src/modules/llms/models-modal/ModelsWizard.tsx +++ b/src/modules/llms/models-modal/ModelsWizard.tsx @@ -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 = [ + { 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>, + 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>, + 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(null); + const [localValue, setLocalValue] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); const [updateError, setUpdateError] = React.useState(null); // external state const stabilizeTransportAccess = useShallowStabilizer>(); - 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 : ( {/**/} {/* */} @@ -144,17 +179,26 @@ function WizardProviderSetup(props: { {/**/} {/**/} - ), [handleSetServiceKey, localKey, serviceAPIKey]); + ), [handleSetServiceKeyValue, localValue, serviceKeyValue]); - return ( + // heuristics for warnings + const isOnLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost'; + + return props.isHidden ? null : providerOmit ? ( + + {!isOnLocalhost && + 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. + } + + ) : ( @@ -186,13 +230,13 @@ function WizardProviderSetup(props: { {/* Line 2 */} } endDecorator={endButtons} @@ -206,7 +250,7 @@ function WizardProviderSetup(props: { {/*{!isLoading && !updateError && !!llmsCount && (*/} {/* {llmsCount} models added.*/} {/*)}*/} - {!isLoading && !updateError && !serviceLLMsCount && !!serviceAPIKey && ( + {!isLoading && !updateError && !serviceLLMsCount && !!serviceKeyValue && ( No models found. )} {!!updateError && {updateError}} @@ -223,21 +267,24 @@ export function ModelsWizard(props: { }) { // state - // const [category, setCategory] = React.useState<'popular' | 'local'>('popular'); + const [activeCategory, setActiveCategory] = React.useState('popular'); + + // derived + const isLocal = activeCategory === 'local'; return ( - - - Enter API keys to connect your AI services. - {/* setCategory('popular')}>*/} - {/* popular*/} - {/**/} - {/* setCategory('popular')}>*/} - {/* local*/} - {/**/} - {/*AI services.*/} + + + Enter {isLocal ? 'the addresses of ' : 'your API keys for '} + setActiveCategory('popular')}> + Popular + + setActiveCategory('local')}> + Local + + {' '}AI services below. {/**/} {/* Enter API keys to connect your AI services.{' '}*/} @@ -245,13 +292,25 @@ export function ModelsWizard(props: { {/**/} - {WizardVendors.map(({ vendor, apiKeyField }, index) => ( - + {WizardProviders.map((provider, index) => ( + ))} - + {/*{!props.isMobile && <>Switch to Advanced to choose between {getModelVendorsCount()} services.}{' '}*/} - {!props.isMobile && <>Switch to Advanced for more services,}{' '} + {!props.isMobile && <> + Switch to{' '} + advanced configuration + {/**/} + {/* more services*/} + {/**/} + {' '}for more services, + }{' '} or skip for now and do it later.