some UI for models!

This commit is contained in:
Andrew Pareles 2024-12-16 21:49:03 -08:00
parent c44497510b
commit a7aab9cf86
5 changed files with 189 additions and 89 deletions

View file

@ -75,7 +75,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this._cancelTimeout() this._cancelTimeout()
// if ollama is disabled, obivously done // if ollama is disabled, obivously done
if (this.voidSettingsService.state.settingsOfProvider.ollama.enabled !== 'true') { if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) {
this._setState('done') this._setState('done')
return return
} }

View file

@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../storage/comm
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js'; import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidSettings' const STORAGE_KEY = 'void.voidSettingsI'
type SetSettingOfProviderFn = <S extends SettingName>( type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName, providerName: ProviderName,
@ -60,7 +60,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = [] let modelOptions: ModelOption[] = []
for (const providerName of providerNames) { for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName] const providerConfig = settingsOfProvider[providerName]
if (providerConfig.enabled !== 'true') continue if (!providerConfig.enabled) continue // if disabled, don't display model options
for (const { modelName, isHidden } of providerConfig.models) { for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue if (isHidden) continue
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } }) modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })

View file

@ -5,6 +5,8 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
export type ModelInfo = { export type ModelInfo = {
modelName: string, modelName: string,
isDefault: boolean, // whether or not it's a default for its provider isDefault: boolean, // whether or not it's a default for its provider
@ -90,16 +92,11 @@ export const anthropicMaxPossibleTokens = (modelName: string) => {
} }
// export const dummyModelData = { type UnionOfKeys<T> = T extends T ? keyof T : never;
// anthropic: ['claude 3.5'],
// openAI: ['gpt 4o'],
// ollama: ['llama 3.2', 'codestral'],
// openRouter: ['qwen 2.5'],
// }
export const voidProviderDefaults = { export const customProviderSettingsDefaults = {
anthropic: { anthropic: {
apiKey: '', apiKey: '',
}, },
@ -124,57 +121,30 @@ export const voidProviderDefaults = {
} }
} as const } as const
export type ProviderName = keyof typeof customProviderSettingsDefaults
export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[]
export const voidInitModelOptions = {
anthropic: { type CustomSettingName = UnionOfKeys<typeof customProviderSettingsDefaults[ProviderName]>
models: defaultAnthropicModels,
}, type CustomProviderSettings<providerName extends ProviderName> = {
openAI: { [k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined
models: defaultOpenAIModels,
},
ollama: {
models: [],
},
openRouter: {
models: [], // any string
},
openAICompatible: {
models: [],
},
gemini: {
models: defaultGeminiModels,
},
groq: {
models: defaultGroqModels,
},
} }
type CommonProviderSettings = {
enabled: boolean,
models: ModelInfo[], // if null, user can type in any string as a model
}
type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
export type ProviderName = keyof typeof voidProviderDefaults // part of state
export const providerNames = Object.keys(voidProviderDefaults) as ProviderName[]
// state
export type SettingsOfProvider = { export type SettingsOfProvider = {
[providerName in ProviderName]: ( [providerName in ProviderName]: SettingsForProvider<providerName>
{
[optionName in keyof typeof voidProviderDefaults[providerName]]: string
}
&
{
enabled: string, // 'true' | 'false'
models: ModelInfo[], // if null, user can type in any string as a model
})
} }
type UnionOfKeys<T> = T extends T ? keyof T : never; export type SettingName = keyof SettingsForProvider<ProviderName>
export type SettingName = UnionOfKeys<SettingsOfProvider[ProviderName]>
@ -219,7 +189,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
title: providerName === 'ollama' ? 'Your Ollama endpoint' : title: providerName === 'ollama' ? 'Your Ollama endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)', : '(never)',
placeholder: providerName === 'ollama' ? voidProviderDefaults.ollama.endpoint placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1' : providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)', : '(never)',
} }
@ -242,42 +212,81 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
} }
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
}
export const voidInitModelOptions = {
anthropic: {
models: defaultAnthropicModels,
},
openAI: {
models: defaultOpenAIModels,
},
ollama: {
models: [],
},
openRouter: {
models: [], // any string
},
openAICompatible: {
models: [],
},
gemini: {
models: defaultGeminiModels,
},
groq: {
models: defaultGroqModels,
},
}
// used when waiting and for a type reference // used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = { export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: { anthropic: {
...voidProviderDefaults.anthropic, ...defaultCustomSettings,
...customProviderSettingsDefaults.anthropic,
...voidInitModelOptions.anthropic, ...voidInitModelOptions.anthropic,
enabled: 'false', enabled: false,
}, },
openAI: { openAI: {
...voidProviderDefaults.openAI, ...defaultCustomSettings,
...customProviderSettingsDefaults.openAI,
...voidInitModelOptions.openAI, ...voidInitModelOptions.openAI,
enabled: 'false', enabled: false,
}, },
gemini: { gemini: {
...voidProviderDefaults.gemini, ...defaultCustomSettings,
...customProviderSettingsDefaults.gemini,
...voidInitModelOptions.gemini, ...voidInitModelOptions.gemini,
enabled: 'false', enabled: false,
}, },
groq: { groq: {
...voidProviderDefaults.groq, ...defaultCustomSettings,
...customProviderSettingsDefaults.groq,
...voidInitModelOptions.groq, ...voidInitModelOptions.groq,
enabled: 'false', enabled: false,
}, },
ollama: { ollama: {
...voidProviderDefaults.ollama, ...defaultCustomSettings,
...customProviderSettingsDefaults.ollama,
...voidInitModelOptions.ollama, ...voidInitModelOptions.ollama,
enabled: 'false', enabled: false,
}, },
openRouter: { openRouter: {
...voidProviderDefaults.openRouter, ...defaultCustomSettings,
...customProviderSettingsDefaults.openRouter,
...voidInitModelOptions.openRouter, ...voidInitModelOptions.openRouter,
enabled: 'false', enabled: false,
}, },
openAICompatible: { openAICompatible: {
...voidProviderDefaults.openAICompatible, ...defaultCustomSettings,
...customProviderSettingsDefaults.openAICompatible,
...voidInitModelOptions.openAICompatible, ...voidInitModelOptions.openAICompatible,
enabled: 'false', enabled: false,
}, },
} }

View file

@ -43,7 +43,6 @@ const DummySelectBox = () => {
return <VoidSelectBox return <VoidSelectBox
options={[{ text: 'Please add a model!', value: null }]} options={[{ text: 'Please add a model!', value: null }]}
onChangeSelection={() => { }} onChangeSelection={() => { }}
onCreateInstance={() => { }}
/> />
} }

View file

@ -1,8 +1,8 @@
import React, { useCallback } from 'react' import React, { useCallback, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidInputBox } from '../util/inputs.js' import { VoidInputBox, VoidSelectBox } from '../util/inputs.js'
import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js' import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
@ -15,7 +15,7 @@ const RefreshableModels = () => {
const refreshModelState = useRefreshModelState() const refreshModelState = useRefreshModelState()
const refreshModelService = useService('refreshModelService') const refreshModelService = useService('refreshModelService')
if (settingsState.settingsOfProvider.ollama.enabled !== 'true') if (!settingsState.settingsOfProvider.ollama.enabled)
return null return null
return <> return <>
@ -26,8 +26,82 @@ const RefreshableModels = () => {
const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const settingsStateService = useService('settingsStateService')
const settingsState = useSettingsState()
export const ModelMenu = () => { const providerNameRef = useRef<ProviderName | null>(null)
const modelNameRef = useRef<string | null>(null)
const [errorString, setErrorString] = useState('')
const providerOptions = providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName }))
return <div className='flex justify-center items-center gap-20'>
{/* model */}
<div className='max-w-xl w-full'>
<VoidInputBox
placeholder='Model Name'
onChangeText={(modelName) => { modelNameRef.current = modelName }}
multiline={false}
/>
</div>
{/* provider */}
<div className='w-fit'>
<VoidSelectBox
onCreateInstance={(instance) => { providerNameRef.current = providerOptions[0].value }} // initialize state
onChangeSelection={(providerName: ProviderName) => { console.log('selecting provider', providerName); providerNameRef.current = providerName }}
options={providerOptions}
/>
</div>
{/* button */}
<div className='w-80'>
<button onClick={() => {
const providerName = providerNameRef.current
const modelName = modelNameRef.current
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists under ${providerName}.`)
return
}
settingsStateService.addModel(providerName, modelName)
onSubmit()
}}>Add model</button>
</div>
{!errorString ? null : <>
{errorString}
</>}
</div>
}
const AddModelButton = () => {
const [open, setOpen] = useState(false)
return <>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button onClick={() => setOpen(true)}>Add Model</button>
}
</>
}
export const ModelDump = () => {
const settingsStateService = useService('settingsStateService') const settingsStateService = useService('settingsStateService')
const settingsState = useSettingsState() const settingsState = useSettingsState()
@ -36,21 +110,30 @@ export const ModelMenu = () => {
const modelDump: (ModelInfo & { providerName: ProviderName })[] = [] const modelDump: (ModelInfo & { providerName: ProviderName })[] = []
for (let providerName of providerNames) { for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName] const providerSettings = settingsState.settingsOfProvider[providerName]
if (providerSettings.enabled !== 'true') continue if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName }))) modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName })))
} }
return <> return <div className='max-h-80 overflow-y-auto'>
{modelDump.map(m => { {modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName } = m const { isHidden, isDefault, modelName, providerName } = m
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-white/10'> return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden cursor-default'>
<span>{modelName} {isDefault ? '' : '(custom)'}</span> {/* left part is width:full */}
<span>{providerName}</span> <div className='w-full flex items-center gap-4'>
<span onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{isHidden ? 'hidden' : '✅'}</span> <span>{`${modelName} (${providerName})`}</span>
</div>
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
<div onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{isHidden ? '❌' : '✅'}</div>
<div className='w-10'>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}>x</button>}
</div>
</div>
</div> </div>
})} })}
</> </div>
} }
@ -78,7 +161,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
onCreateInstance={useCallback((instance: InputBox) => { onCreateInstance={useCallback((instance: InputBox) => {
const syncInstance = () => { const syncInstance = () => {
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName]; const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
const stateVal = settingsAtProvider[settingName as keyof typeof settingsAtProvider] const stateVal = settingsAtProvider[settingName as SettingName]
// console.log('SYNCING TO', providerName, settingName, stateVal) // console.log('SYNCING TO', providerName, settingName, stateVal)
weChangedTextRef = true weChangedTextRef = true
instance.value = stateVal as string instance.value = stateVal as string
@ -96,10 +179,16 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidSettingsState = useSettingsState() const voidSettingsState = useSettingsState()
const { models, ...others } = voidSettingsState.settingsOfProvider[providerName] const voidSettingsService = useService('settingsStateService')
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName]
return <> return <>
<h1 className='text-xl'>{titleOfProviderName(providerName)}</h1>
<div className='flex items-center gap-4'>
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
<span onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</span>
</div>
{/* settings besides models (e.g. api key) */} {/* settings besides models (e.g. api key) */}
{Object.keys(others).map((sName, i) => { {Object.keys(others).map((sName, i) => {
const settingName = sName as keyof typeof others const settingName = sName as keyof typeof others
@ -123,19 +212,22 @@ export const VoidProviderSettings = () => {
export const Settings = () => { export const Settings = () => {
const isDark = useIsDark() const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''} px-2 lg:px-10`}> return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div className='w-full h-full'> <div className='w-full h-full px-10 py-10'>
<div className='max-w-3xl mx-auto'> <div className='max-w-3xl mx-auto'>
<h2 className='text-3xl'>Models</h2>
<ErrorBoundary> <ErrorBoundary>
<ModelMenu /> <ModelDump />
<AddModelButton />
<RefreshableModels /> <RefreshableModels />
</ErrorBoundary> </ErrorBoundary>
</div>
<ErrorBoundary> <h2 className='text-3xl'>Providers</h2>
<VoidProviderSettings /> <ErrorBoundary>
</ErrorBoundary> <VoidProviderSettings />
</ErrorBoundary>
</div>
</div> </div>
</div> </div>