mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
auto fetch model works
This commit is contained in:
parent
c3c53b5e81
commit
81b78b7765
4 changed files with 231 additions and 92 deletions
|
|
@ -8,7 +8,7 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common
|
|||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
|
||||
|
|
@ -17,12 +17,27 @@ export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies
|
|||
|
||||
export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
type ModelRefreshState = 'nothing' | 'refreshing' | 'success'
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, {
|
||||
state: ModelRefreshState,
|
||||
timeoutId: NodeJS.Timeout | null // not really part of state
|
||||
}>
|
||||
|
||||
type RefreshableState = {
|
||||
state: 'init',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'refreshing',
|
||||
timeoutId: NodeJS.Timeout | null,
|
||||
} | {
|
||||
state: 'success',
|
||||
timeoutId: null,
|
||||
}
|
||||
|
||||
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['enabled', 'endpoint'],
|
||||
openAICompatible: ['enabled', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5000
|
||||
|
||||
// element-wise equals
|
||||
|
|
@ -55,52 +70,63 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
) {
|
||||
super()
|
||||
|
||||
// on mount, start refreshing models if there are no defaults
|
||||
const refreshables: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['enabled', 'endpoint'],
|
||||
openAICompatible: ['enabled', 'endpoint', 'apiKey'],
|
||||
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
|
||||
const startRefreshing = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
||||
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
|
||||
|
||||
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()
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
refresh()
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const p in refreshables) {
|
||||
const providerName = p as keyof typeof refreshables
|
||||
this.refreshModels(providerName)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like useEffect
|
||||
let relevantVals = () => refreshables[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
// 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()
|
||||
this._register(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
this.refreshModels(providerName)
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'nothing', timeoutId: null },
|
||||
openAICompatible: { state: 'nothing', timeoutId: null },
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
openAICompatible: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
async refreshModels(providerName: RefreshableProviderName) {
|
||||
// cancel any existing poll
|
||||
if (this.state[providerName].timeoutId) {
|
||||
clearTimeout(this.state[providerName].timeoutId)
|
||||
this._setTimeoutId(providerName, null)
|
||||
}
|
||||
|
||||
// if provider is disabled, obivously done
|
||||
if (!this.voidSettingsService.state.settingsOfProvider[providerName].enabled) {
|
||||
this._setIsRefreshing(providerName, 'nothing')
|
||||
return
|
||||
}
|
||||
// start listening for models (and don't stop until success)
|
||||
async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
// start loading models
|
||||
this._setIsRefreshing(providerName, 'refreshing')
|
||||
this._setRefreshState(providerName, 'refreshing')
|
||||
|
||||
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
|
|
@ -113,22 +139,40 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName)
|
||||
}))
|
||||
this._setIsRefreshing(providerName, 'success')
|
||||
|
||||
if (options?.enableProviderOnSuccess)
|
||||
this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true)
|
||||
|
||||
this._setRefreshState(providerName, 'success')
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
// poll
|
||||
console.log('retrying list models:', providerName, error)
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName), REFRESH_INTERVAL)
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_clearAllTimeouts() {
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
_clearProviderTimeout(providerName: RefreshableProviderName) {
|
||||
// cancel any existing poll
|
||||
if (this.state[providerName].timeoutId) {
|
||||
clearTimeout(this.state[providerName].timeoutId)
|
||||
this._setTimeoutId(providerName, null)
|
||||
}
|
||||
}
|
||||
|
||||
private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) {
|
||||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setIsRefreshing(providerName: RefreshableProviderName, state: ModelRefreshState) {
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js
|
|||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
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, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.voidSettingsI'
|
||||
|
|
@ -21,13 +21,13 @@ type SetSettingOfProviderFn = <S extends SettingName>(
|
|||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeature = <K extends FeatureName>(
|
||||
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
|
||||
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
|
||||
|
|
@ -36,18 +36,24 @@ export type ModelOption = { text: string, value: ModelSelection }
|
|||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly featureFlagSettings: FeatureFlagSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState;
|
||||
onDidChangeState: Event<void>;
|
||||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setFeatureFlag: SetFeatureFlagFn;
|
||||
|
||||
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
|
|
@ -74,6 +80,7 @@ const defaultState = () => {
|
|||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
|
|
@ -84,10 +91,11 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
|
|||
class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
private readonly _onDidChangeState = new Emitter<EventProp>();
|
||||
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
|
|
@ -100,10 +108,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
let resolver: () => void = () => { }
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
this._onDidChangeState.fire()
|
||||
resolver()
|
||||
this._onDidChangeState.fire('all')
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +148,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
}
|
||||
|
||||
const newFeatureFlags = this.state.featureFlagSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === 'enabled'
|
||||
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
|
||||
|
|
@ -143,6 +157,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
featureFlagSettings: newFeatureFlags,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
|
|
@ -166,11 +181,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onDidChangeState.fire('settingsOfProvider')
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => {
|
||||
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
|
||||
const newState = {
|
||||
...this.state,
|
||||
featureFlagSettings: {
|
||||
...this.state.featureFlagSettings,
|
||||
[flagName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('featureFlagSettings')
|
||||
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
|
|
@ -185,7 +215,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onDidChangeState.fire('modelSelectionOfFeature')
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -203,7 +233,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newModels: ModelInfo[] = [
|
||||
const newModels: VoidModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
|
||||
|
||||
export type ModelInfo = {
|
||||
export type VoidModelInfo = {
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it
|
||||
}
|
||||
|
||||
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[]): ModelInfo[] => {
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => {
|
||||
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden }))
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
|
|||
|
||||
|
||||
|
||||
export const customProviderSettingsDefaults = {
|
||||
export const customProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
|
|
@ -121,19 +121,20 @@ export const customProviderSettingsDefaults = {
|
|||
}
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof customProviderSettingsDefaults
|
||||
export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[]
|
||||
|
||||
export type ProviderName = keyof typeof customProviderSettings
|
||||
export const providerNames = Object.keys(customProviderSettings) as ProviderName[]
|
||||
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof customProviderSettingsDefaults[ProviderName]>
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof customProviderSettings[ProviderName]>
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined
|
||||
[k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined
|
||||
}
|
||||
|
||||
type CommonProviderSettings = {
|
||||
enabled: boolean,
|
||||
models: ModelInfo[], // if null, user can type in any string as a model
|
||||
enabled: boolean | undefined, // undefined initially
|
||||
models: VoidModelInfo[],
|
||||
}
|
||||
|
||||
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
|
@ -148,6 +149,14 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
|
|||
|
||||
|
||||
|
||||
|
||||
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||
return Object.keys(customProviderSettings[providerName]) as CustomSettingName[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const titleOfProviderName = (providerName: ProviderName) => {
|
||||
if (providerName === 'anthropic')
|
||||
return 'Anthropic'
|
||||
|
|
@ -203,7 +212,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint
|
||||
placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
|
|
@ -267,46 +276,46 @@ export const voidInitModelOptions = {
|
|||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.anthropic,
|
||||
...customProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
enabled: false,
|
||||
},
|
||||
openAI: {
|
||||
enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAI,
|
||||
...customProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
enabled: false,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.gemini,
|
||||
...customProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
enabled: false,
|
||||
enabled: undefined,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.groq,
|
||||
...customProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
enabled: false,
|
||||
enabled: undefined,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.ollama,
|
||||
...customProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
enabled: false,
|
||||
enabled: undefined,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openRouter,
|
||||
...customProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
enabled: false,
|
||||
enabled: undefined,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAICompatible,
|
||||
...customProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
enabled: false,
|
||||
enabled: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -326,3 +335,35 @@ export type ModelSelectionOfFeature = {
|
|||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type FeatureFlagSettings = {
|
||||
autoRefreshModels: boolean; // automatically scan for local models and enable when found
|
||||
}
|
||||
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
|
||||
autoRefreshModels: true,
|
||||
}
|
||||
|
||||
export type FeatureFlagName = keyof FeatureFlagSettings
|
||||
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
|
||||
|
||||
type FeatureFlagDisplayInfo = {
|
||||
description: string,
|
||||
}
|
||||
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
|
||||
if (featureFlag === 'autoRefreshModels') {
|
||||
return {
|
||||
description: 'Automatically scan for and enable local models.',
|
||||
}
|
||||
}
|
||||
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, 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 { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidInputBox, VoidSelectBox } from '../util/inputs.js'
|
||||
import { useIsDark, useRefreshModelListener, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
|
||||
|
|
@ -123,15 +123,15 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
|
||||
}
|
||||
|
||||
const AddModelButton = () => {
|
||||
const AddModelMenuFull = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return <>
|
||||
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 onClick={() => setOpen(true)}>Add Model</button>
|
||||
: <button className='' onClick={() => setOpen(true)}>Add Model</button>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -141,11 +141,11 @@ export const ModelDump = () => {
|
|||
const settingsState = useSettingsState()
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (ModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: providerSettings.enabled })))
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings.enabled })))
|
||||
}
|
||||
|
||||
return <div className=''>
|
||||
|
|
@ -160,7 +160,11 @@ export const ModelDump = () => {
|
|||
{/* 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 || isHidden) ? '❌' : '✅'}</button>
|
||||
<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'>
|
||||
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||
</div>
|
||||
|
|
@ -216,17 +220,16 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
|||
const voidSettingsState = useSettingsState()
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
|
||||
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[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>
|
||||
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
|
||||
</div>
|
||||
{/* settings besides models (e.g. api key) */}
|
||||
{Object.keys(others).map((sName, i) => {
|
||||
const settingName = sName as keyof typeof others
|
||||
{settingNames.map((settingName, i) => {
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
</>
|
||||
|
|
@ -242,6 +245,26 @@ export const VoidProviderSettings = () => {
|
|||
}
|
||||
|
||||
|
||||
export const VoidFeatureFlagSettings = () => {
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
return <>
|
||||
{featureFlagNames.map((flagName) => {
|
||||
const value = voidSettingsState.featureFlagSettings[flagName]
|
||||
const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
return <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<button onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}>
|
||||
{value ? '✅' : '❌'}
|
||||
</button>
|
||||
<h4 className='text-sm'>{description}</h4>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
// full settings
|
||||
|
||||
|
|
@ -283,7 +306,7 @@ export const Settings = () => {
|
|||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<ModelDump />
|
||||
<AddModelButton />
|
||||
<AddModelMenuFull />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
<h2 className={`text-3xl mt-4 mb-2`}>Providers</h2>
|
||||
|
|
@ -296,6 +319,7 @@ export const Settings = () => {
|
|||
|
||||
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
|
||||
<VoidFeatureFlagSettings />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue