settings page styles

This commit is contained in:
Andrew Pareles 2024-12-19 04:05:31 -08:00
parent 3669401819
commit f457f006fb
8 changed files with 129 additions and 103 deletions

View file

@ -9,25 +9,22 @@ import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export type RefreshableProviderName = typeof refreshableProviderNames[number]
type RefreshableState = {
type RefreshableState = ({
state: 'init',
timeoutId: null,
} | {
state: 'refreshing',
timeoutId: NodeJS.Timeout | null,
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
} | {
state: 'success',
timeoutId: null,
}
})
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
@ -38,7 +35,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide
ollama: ['enabled', 'endpoint'],
openAICompatible: ['enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5000
const REFRESH_INTERVAL = 5_000
const COOLDOWN_TIMEOUT = 1_000
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
@ -64,6 +62,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
readonly onDidChangeState: Event<RefreshableProviderName> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidAutoEnable = new Emitter<RefreshableProviderName>();
constructor(
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
@ -73,8 +73,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
const disposables: Set<IDisposable> = new Set()
const startRefreshing = () => {
const initializePollingAndOnChange = () => {
this._clearAllTimeouts()
disposables.forEach(d => d.dispose())
disposables.clear()
@ -83,12 +82,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
const refresh = () => {
// const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, { enableProviderOnSuccess: true }) // enable the provider on success
}
refresh()
const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled)
// every time providerName.enabled changes, refresh models too, like a useEffect
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
@ -97,8 +92,18 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
refresh()
prevVals = newVals
const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
if (enabled) {
// if user just clicked enable, refresh
this.refreshModels(providerName, !enabled)
}
else {
// else if user just clicked disable, give 5 seconds cooldown before re-enabling (or at least re-fetching)
const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT)
this._setTimeoutId(providerName, timeoutId)
}
}
})
)
@ -107,9 +112,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
voidSettingsService.waitForInitState.then(() => {
startRefreshing()
initializePollingAndOnChange()
this._register(
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() })
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
)
})
@ -122,7 +127,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// start listening for models (and don't stop until success)
async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) {
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) {
this._clearProviderTimeout(providerName)
// start loading models
@ -140,15 +145,17 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
else throw new Error('refreshMode fn: unknown provider', providerName)
}))
if (options?.enableProviderOnSuccess)
if (enableProviderOnSuccess) {
this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true)
this._onDidAutoEnable.fire(providerName)
}
this._setRefreshState(providerName, 'success')
},
onError: ({ error }) => {
// poll
console.log('retrying list models:', providerName, error)
const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL)
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
})

View file

@ -96,7 +96,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
export const customProviderSettings = {
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
@ -122,15 +122,19 @@ export const customProviderSettings = {
} as const
export type ProviderName = keyof typeof customProviderSettings
export const providerNames = Object.keys(customProviderSettings) as ProviderName[]
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
type CustomSettingName = UnionOfKeys<typeof customProviderSettings[ProviderName]>
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
}
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
type CommonProviderSettings = {
enabled: boolean | undefined, // undefined initially
@ -150,11 +154,6 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(customProviderSettings[providerName]) as CustomSettingName[]
}
export const titleOfProviderName = (providerName: ProviderName) => {
@ -208,11 +207,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
@ -278,42 +277,42 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
enabled: undefined,
...defaultCustomSettings,
...customProviderSettings.anthropic,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
},
openAI: {
enabled: undefined,
...defaultCustomSettings,
...customProviderSettings.openAI,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
},
gemini: {
...defaultCustomSettings,
...customProviderSettings.gemini,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
enabled: undefined,
},
groq: {
...defaultCustomSettings,
...customProviderSettings.groq,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...customProviderSettings.ollama,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...customProviderSettings.openRouter,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...customProviderSettings.openAICompatible,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
enabled: undefined,
},
@ -341,11 +340,19 @@ export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export type RefreshableProviderName = typeof refreshableProviderNames[number]
export type FeatureFlagSettings = {
autoRefreshModels: boolean; // automatically scan for local models and enable when found
autoRefreshModels: boolean;
}
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
autoRefreshModels: true,
@ -360,7 +367,7 @@ type FeatureFlagDisplayInfo = {
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
if (featureFlag === 'autoRefreshModels') {
return {
description: 'Automatically scan for and enable local models.',
description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
}
}
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)

View file

@ -5,6 +5,7 @@
import { Ollama } from 'ollama';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
@ -18,6 +19,9 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async (
try {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.list()
.then((response) => {
@ -38,6 +42,8 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async (
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
let fullText = ''

View file

@ -483,7 +483,7 @@ export const SidebarChat = () => {
// .split(' ')
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // apply styles to ancestor input and textarea elements
// .join(' ');
`@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px]@@[&_div.monaco-inputbox]:!void- @@[&_div.monaco-inputbox]:!void-outline-none`
`@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px] @@[&_div.monaco-inputbox]:!void-outline-none`
}
>

View file

@ -16,6 +16,11 @@
}
}
* {
outline: none !important;
}
/* html {
font-size: var(--vscode-font-size);

View file

@ -96,15 +96,15 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
export const VoidSwitch = ({
value,
onChange,
size = 'md',
label,
disabled = false,
size = 'md'
}: {
value: boolean;
onChange: (value: boolean) => void;
label?: string;
disabled?: boolean;
size?: 'xs' | 'sm' | 'msm' | 'md';
size?: 'xs' | 'sm' | 'sm+' | 'md';
}) => {
return (
<label className="inline-flex items-center cursor-pointer">
@ -113,10 +113,10 @@ export const VoidSwitch = ({
className={`
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
${disabled ? 'opacity-25' : ''}
${size === 'xs' ? 'h-4 w-7' : ''}
${size === 'sm' ? 'h-5 w-9' : ''}
${size === 'msm' ? 'h-5 w-10' : ''}
${size === 'sm+' ? 'h-5 w-10' : ''}
${size === 'md' ? 'h-6 w-11' : ''}
`}
>
@ -125,11 +125,11 @@ export const VoidSwitch = ({
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
${size === 'sm' ? 'h-3 w-3' : ''}
${size === 'msm' ? 'h-3.5 w-3.5' : ''}
${size === 'sm+' ? 'h-3.5 w-3.5' : ''}
${size === 'md' ? 'h-4 w-4' : ''}
${size === 'xs' ? (value ? 'translate-x-3.5' : 'translate-x-0.5') : ''}
${size === 'sm' ? (value ? 'translate-x-5' : 'translate-x-1') : ''}
${size === 'msm' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
${size === 'sm+' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
${size === 'md' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
`}
/>

View file

@ -5,13 +5,13 @@
import { useState, useEffect } from 'react'
import { ThreadsState } from '../../../threadHistoryService.js'
import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { RefreshableProviderName, RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes

View file

@ -1,11 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { useIsDark, useRefreshModelListener, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
import { RefreshableProviderName, refreshableProviderNames } from '../../../../../../../platform/void/common/refreshModelService.js'
@ -69,7 +68,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName })), [providerNames])
return <>
<div className='flex justify-center items-center gap-4'>
<div className='flex items-center gap-4'>
{/* model */}
<div className='max-w-40 w-full'>
<VoidInputBox
@ -89,8 +88,9 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
</div>
{/* button */}
<div className='max-w-40 w-full'>
<div className='max-w-40'>
<button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => {
const providerName = providerNameRef.current
const modelName = modelNameRef.current
@ -114,11 +114,14 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
}}>Add model</button>
</div>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
{errorString}
</div>}
</div>
{!errorString ? null : <div className='text-center text-red-500'>
{errorString}
</div>}
</>
}
@ -129,7 +132,10 @@ const AddModelMenuFull = () => {
return <div className='my-2 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button className='' onClick={() => setOpen(true)}>Add Model</button>
: <button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => setOpen(true)}
>Add Model</button>
}
</div>
}
@ -148,24 +154,32 @@ export const ModelDump = () => {
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings.enabled })))
}
// sort by hidden
modelDump.sort((a, b) => {
return Number(b.providerEnabled) - Number(a.providerEnabled)
})
return <div className=''>
{modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
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'>
<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>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>
{!providerEnabled ? '🌑' // provider disabled
: isHidden ? '❌' // model is disabled
: '✅'}
</button>
<div className='w-5 flex items-center justify-center'>
<VoidSwitch
value={!isHidden}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={!providerEnabled}
size='sm'
/>
<div className={`w-5 flex items-center justify-center`}>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
</div>
@ -180,16 +194,16 @@ export const ModelDump = () => {
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const { title, placeholder, } = displayInfoOfSettingName(providerName, settingName)
const providerTitle = titleOfProviderName(providerName)
const { title: settingTitle, placeholder, } = displayInfoOfSettingName(providerName, settingName)
const voidSettingsService = useService('settingsStateService')
let weChangedTextRef = false
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
placeholder={`Enter your ${title} here (${placeholder}).`}
placeholder={`Enter your ${providerTitle} ${settingTitle} (${placeholder}).`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
@ -213,7 +227,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
/>
</div>
</ErrorBoundary>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
@ -223,9 +236,10 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
const { enabled } = voidSettingsState.settingsOfProvider[providerName]
const settingNames = customSettingNamesOfProvider(providerName)
return <>
<div className='flex items-center gap-4'>
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
return <div className='my-4'>
<div className='flex items-center w-full gap-4'>
<h3 className='text-xl truncate'>{titleOfProviderName(providerName)}</h3>
{/* enable provider switch */}
<VoidSwitch
value={!!enabled}
onChange={
@ -233,28 +247,16 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
}, [voidSettingsService, providerName])}
size='xs'
disabled={false}
label=''
size='sm+'
/>
{/* <VoidCheckBox
checked={!!enabled}
onChange={
useCallback(() => {
const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
}, [voidSettingsService, providerName])}
/> */}
{/* <button className='flex items-center'
onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}
>{enabled ? '✅' : '❌'}</button> */}
</div>
{/* settings besides models (e.g. api key) */}
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
<div className='px-0'>
{/* settings besides models (e.g. api key) */}
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</div>
</div>
}
@ -311,10 +313,10 @@ export const Settings = () => {
{/* tabs */}
<div className='flex flex-col w-full max-w-32'>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('models') }}
>Models</button>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('features') }}
>Features</button>
</div>
@ -327,18 +329,17 @@ export const Settings = () => {
<div className='w-full overflow-y-auto'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`}>Models</h2>
<h2 className={`text-3xl mb-2`}>Providers</h2>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-4`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelMenuFull />
<RefreshableModels />
</ErrorBoundary>
<h2 className={`text-3xl mt-4 mb-2`}>Providers</h2>
<div className='px-3'>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>