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()
// if ollama is disabled, obivously done
if (this.voidSettingsService.state.settingsOfProvider.ollama.enabled !== 'true') {
if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) {
this._setState('done')
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';
const STORAGE_KEY = 'void.voidSettings'
const STORAGE_KEY = 'void.voidSettingsI'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
@ -60,7 +60,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
for (const providerName of providerNames) {
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) {
if (isHidden) continue
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })

View file

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

View file

@ -43,7 +43,6 @@ const DummySelectBox = () => {
return <VoidSelectBox
options={[{ text: 'Please add a model!', value: null }]}
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 { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.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'
@ -15,7 +15,7 @@ const RefreshableModels = () => {
const refreshModelState = useRefreshModelState()
const refreshModelService = useService('refreshModelService')
if (settingsState.settingsOfProvider.ollama.enabled !== 'true')
if (!settingsState.settingsOfProvider.ollama.enabled)
return null
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 settingsState = useSettingsState()
@ -36,21 +110,30 @@ export const ModelMenu = () => {
const modelDump: (ModelInfo & { providerName: ProviderName })[] = []
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
if (providerSettings.enabled !== 'true') continue
if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName })))
}
return <>
return <div className='max-h-80 overflow-y-auto'>
{modelDump.map(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'>
<span>{modelName} {isDefault ? '' : '(custom)'}</span>
<span>{providerName}</span>
<span onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{isHidden ? 'hidden' : '✅'}</span>
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'>
{/* left part is width:full */}
<div className='w-full flex items-center gap-4'>
<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>
}
@ -78,7 +161,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
onCreateInstance={useCallback((instance: InputBox) => {
const syncInstance = () => {
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)
weChangedTextRef = true
instance.value = stateVal as string
@ -96,10 +179,16 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidSettingsState = useSettingsState()
const { models, ...others } = voidSettingsState.settingsOfProvider[providerName]
const voidSettingsService = useService('settingsStateService')
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName]
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) */}
{Object.keys(others).map((sName, i) => {
const settingName = sName as keyof typeof others
@ -123,19 +212,22 @@ export const VoidProviderSettings = () => {
export const Settings = () => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''} px-2 lg:px-10`}>
<div className='w-full h-full'>
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div className='w-full h-full px-10 py-10'>
<div className='max-w-3xl mx-auto'>
<h2 className='text-3xl'>Models</h2>
<ErrorBoundary>
<ModelMenu />
<ModelDump />
<AddModelButton />
<RefreshableModels />
</ErrorBoundary>
</div>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
<h2 className='text-3xl'>Providers</h2>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
</div>
</div>