new model dropdown draft

This commit is contained in:
Mathew Pareles 2025-01-08 22:06:58 -08:00
parent ec65903692
commit 865c34f282
5 changed files with 149 additions and 38 deletions

View file

@ -29,7 +29,7 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
export type ModelOption = { text: string, value: ModelSelection }
export type ModelOption = { name: string, selection: ModelSelection }
@ -69,7 +69,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
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 } })
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
}
}
return modelOptions
@ -169,11 +169,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}

View file

@ -628,7 +628,7 @@ export const SidebarChat = () => {
transition-all duration-200
rounded-md
bg-vscode-input-bg
max-h-[60vh] overflow-y-auto
max-h-[80vh] overflow-y-auto
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
`}
onKeyDown={(e) => {

View file

@ -3,7 +3,7 @@
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
@ -13,6 +13,7 @@ import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js'
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
import { useAccessor } from './services.js';
import { ScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js';
// type guard
@ -198,7 +199,95 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
}
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
export const VoidCustomSelectBox = <T extends any>({ options, selectedOption, onChangeOption, getOptionName, getOptionsEqual }: { options: T[], selectedOption?: T, onChangeOption: (newValue: T) => void, getOptionName: (option: T) => string, getOptionsEqual: (a: T, b: T) => boolean }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement | null>(null);
if (!selectedOption) {
selectedOption = options[0]
}
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="relative inline-block" ref={dropdownRef}>
{/* Select Button */}
<button
style={{ fontSize: '6px' }}
className="flex items-center gap-1 px-2 h-4 bg-white hover:bg-gray-100 active:bg-gray-200 whitespace-nowrap"
onClick={() => setIsOpen(!isOpen)}
>
<span>{getOptionName(selectedOption)}</span>
{/* Check Mark */}
<svg className="w-2 h-2 flex-shrink-0" viewBox="0 0 12 12" fill="none">
<path
d="M2.5 4.5L6 8L9.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{/* Dropdown Menu */}
{isOpen && (
<div className="absolute z-10 py-1 mt-1 bg-white border rounded shadow-lg">
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption)
const optionName = getOptionName(option)
return (
<div
key={optionName}
className={`flex items-center h-4 px-2 cursor-pointer whitespace-nowrap text-[6px]
transition-colors duration-100
hover:bg-gray-100 active:bg-gray-200
${thisOptionIsSelected ? 'bg-gray-50' : ''}
`}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="w-2 h-2" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span>{optionName}</span>
</div>
)
})}
</div>
)}
</div>
);
};
export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
onChangeSelection: (value: T) => void;
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
@ -211,9 +300,13 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className={`@@select-child-restyle
className={`
@@select-child-restyle
@@[&_select]:!void-text-void-fg-3
${className ?? ''}`}
@@[&_select]:!void-text-xs
!text-void-fg-3
${className ?? ''}
`}
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
@ -222,7 +315,7 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
options.map(opt => ({ text: opt.text })),
defaultIndex,
contextViewProvider,
defaultSelectBoxStyles
defaultSelectBoxStyles,
] as const;
}, [containerRef, options, contextViewProvider])}

View file

@ -6,7 +6,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { VoidSelectBox } from '../util/inputs.js'
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
@ -17,43 +17,61 @@ import { ModelOption } from '../../../../../../../platform/void/common/voidSetti
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
for (let i = 0; i < m1.length; i++) {
if (!modelSelectionsEqual(m1[i].value, m2[i].value)) return false
if (!modelSelectionsEqual(m1[i].selection, m2[i].selection)) return false
}
return true
}
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
let weChangedText = false
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
return <VoidSelectBox
className='@@[&_select]:!void-text-xs text-void-fg-3'
const onChangeOption = useCallback((newOption: ModelOption) => {
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
}, [voidSettingsService, featureName])
return <VoidCustomSelectBox
options={options}
onChangeSelection={useCallback((newVal: ModelSelection) => {
if (weChangedText) return
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
}, [voidSettingsService, featureName])}
// we are responsible for setting the initial state here. always sync instance when state changes.
onCreateInstance={useCallback((instance: SelectBox) => {
const syncInstance = () => {
const modelsListRef = voidSettingsService.state._modelOptions // as a ref
const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
weChangedText = true
instance.select(selectionIdx === -1 ? 0 : selectionIdx)
weChangedText = false
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
}, [voidSettingsService, featureName])}
selectedOption={selectedOption}
onChangeOption={onChangeOption}
getOptionName={(option) => option.name}
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
/>
}
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
// let weChangedText = false
// return <VoidSelectBox
// className='@@[&_select]:!void-text-xs text-void-fg-3'
// options={options}
// onChangeSelection={useCallback((newVal: ModelSelection) => {
// if (weChangedText) return
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
// }, [voidSettingsService, featureName])}
// // we are responsible for setting the initial state here. always sync instance when state changes.
// onCreateInstance={useCallback((instance: SelectBox) => {
// const syncInstance = () => {
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
// weChangedText = true
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
// weChangedText = false
// }
// syncInstance()
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
// return [disposable]
// }, [voidSettingsService, featureName])}
// />
// }
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()

View file

@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { VoidCheckBox, VoidInputBox, _VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
@ -98,7 +98,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
{/* provider */}
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidSelectBox
<_VoidSelectBox
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
options={providerOptions}