From f457f006fb4c9e1720027e9a4e0aba463e68480b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 19 Dec 2024 04:05:31 -0800 Subject: [PATCH] settings page styles --- .../void/common/refreshModelService.ts | 51 +++++---- .../platform/void/common/voidSettingsTypes.ts | 49 +++++---- .../void/electron-main/llmMessage/ollama.ts | 6 + .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/browser/react/src/styles.css | 5 + .../void/browser/react/src/util/inputs.tsx | 12 +- .../void/browser/react/src/util/services.tsx | 4 +- .../react/src/void-settings-tsx/Settings.tsx | 103 +++++++++--------- 8 files changed, 129 insertions(+), 103 deletions(-) diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index e552e6d0..945ca4cb 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -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 @@ -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(a: T[], b: T[]): boolean { @@ -64,6 +62,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidAutoEnable = new Emitter(); + 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 = 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) } }) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 5e47fee5..a02d27fb 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -96,7 +96,7 @@ type UnionOfKeys = 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 +type CustomSettingName = UnionOfKeys type CustomProviderSettings = { - [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 -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}"`) diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index d9184157..48b5fd25 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -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 = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { @@ -18,6 +19,9 @@ export const ollamaList: _InternalModelListFnType = 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 = 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 = '' diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index e5ff34c9..d2e2125a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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` } > diff --git a/src/vs/workbench/contrib/void/browser/react/src/styles.css b/src/vs/workbench/contrib/void/browser/react/src/styles.css index 2ceabda6..ad8b42e3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/void/browser/react/src/styles.css @@ -16,6 +16,11 @@ } } +* { + outline: none !important; +} + + /* html { font-size: var(--vscode-font-size); diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 0c7d039f..3aba6510 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -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 (