All files / src/components/settings/common ModelSelector.tsx

100% Statements 43/43
80% Branches 4/5
50% Functions 1/2
100% Lines 43/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88  1x 1x 1x   1x     1x 1x 41x 41x 41x 1x                 1x                         1x                 1x   1x   1x 1x 8x 8x   8x 8x 8x 8x 8x 8x 8x 8x     8x 8x 8x   8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x             8x  
import { ModelInfo } from "@shared/api"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { DebouncedTextField } from "./DebouncedTextField"
 
/**
 * Container for dropdowns that ensures proper z-index handling
 * This is necessary to ensure dropdown opens downward
 */
export const DropdownContainer = styled.div.attrs<{ zIndex?: number }>(({ zIndex }) => ({
	style: {
		zIndex: zIndex || 1000,
	},
}))`
	position: relative;
 
	// Force dropdowns to open downward
	& vscode-dropdown::part(listbox) {
		position: absolute !important;
		top: 100% !important;
		bottom: auto !important;
	}
`
 
/**
 * Props for the ModelSelector component
 */
interface ModelSelectorProps {
	models: Record<string, ModelInfo>
	selectedModelId: string | undefined
	onChange: (e: any) => void
	zIndex?: number
	label?: string
}
 
/*
OG Saoud Note:
 
	VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing  it with normal select/option elements, which work as expected.
	https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
 
	In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
 
	As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
	*/
 
/**
 * A reusable component for selecting models from a dropdown
 */
export const ModelSelector = ({ models, selectedModelId, onChange, zIndex, label = "Model" }: ModelSelectorProps) => {
	const hasCustomSelection = !!selectedModelId && !(selectedModelId in models)
	const sortedModelIds = Object.keys(models).sort((a, b) => a.localeCompare(b))
 
	return (
		<div style={{ marginBottom: 10 }}>
			<DebouncedTextField
				initialValue={selectedModelId || ""}
				onChange={(value) => onChange({ target: { value } })}
				placeholder="Enter Model ID..."
				style={{ width: "100%", marginBottom: 10 }}>
				<span style={{ fontWeight: 500 }}>Model ID</span>
			</DebouncedTextField>
 
			<DropdownContainer className="dropdown-container" zIndex={zIndex}>
				<label htmlFor="model-id">
					<span style={{ fontWeight: 500 }}>{label}</span>
				</label>
				<VSCodeDropdown id="model-id" onChange={onChange} style={{ width: "100%" }} value={selectedModelId}>
					<VSCodeOption value="">Select a model...</VSCodeOption>
					{hasCustomSelection && <VSCodeOption value={selectedModelId}>{selectedModelId}</VSCodeOption>}
					{sortedModelIds.map((modelId) => (
						<VSCodeOption
							key={modelId}
							style={{
								whiteSpace: "normal",
								wordWrap: "break-word",
								maxWidth: "100%",
							}}
							value={modelId}>
							{modelId}
						</VSCodeOption>
					))}
				</VSCodeDropdown>
			</DropdownContainer>
		</div>
	)
}