Просмотр исходного кода

refactor: unify model picker components

- Extend ModelPicker to support OpenAI models and refreshValues
- Refactor OpenAiModelPicker to use unified ModelPicker component
- Add refreshValues support for Requesty API key
sam hoang 11 месяцев назад
Родитель
Сommit
26a315b946

+ 27 - 8
webview-ui/src/components/settings/ModelPicker.tsx

@@ -25,10 +25,16 @@ import { ModelInfoView } from "./ModelInfoView"
 
 interface ModelPickerProps {
 	defaultModelId: string
-	modelsKey: "glamaModels" | "openRouterModels" | "unboundModels" | "requestyModels"
-	configKey: "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId"
-	infoKey: "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo"
-	refreshMessageType: "refreshGlamaModels" | "refreshOpenRouterModels" | "refreshUnboundModels" | "refreshRequestyModels"
+	modelsKey: "glamaModels" | "openRouterModels" | "unboundModels" | "requestyModels" | "openAiModels"
+	configKey: "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId" | "openAiModelId"
+	infoKey: "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo" | "openAiModelInfo"
+	refreshMessageType:
+		| "refreshGlamaModels"
+		| "refreshOpenRouterModels"
+		| "refreshUnboundModels"
+		| "refreshRequestyModels"
+		| "refreshOpenAiModels"
+	refreshValues?: Record<string, any>
 	serviceName: string
 	serviceUrl: string
 	recommendedModel: string
@@ -40,6 +46,7 @@ export const ModelPicker = ({
 	configKey,
 	infoKey,
 	refreshMessageType,
+	refreshValues,
 	serviceName,
 	serviceUrl,
 	recommendedModel,
@@ -49,7 +56,10 @@ export const ModelPicker = ({
 	const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
 
 	const { apiConfiguration, setApiConfiguration, [modelsKey]: models, onUpdateApiConfig } = useExtensionState()
-	const modelIds = useMemo(() => Object.keys(models).sort((a, b) => a.localeCompare(b)), [models])
+	const modelIds = useMemo(
+		() => (Array.isArray(models) ? models : Object.keys(models)).sort((a, b) => a.localeCompare(b)),
+		[models],
+	)
 
 	const { selectedModelId, selectedModelInfo } = useMemo(
 		() => normalizeApiConfiguration(apiConfiguration),
@@ -58,7 +68,10 @@ export const ModelPicker = ({
 
 	const onSelect = useCallback(
 		(modelId: string) => {
-			const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: models[modelId] }
+			const modelInfo = Array.isArray(models)
+				? { id: modelId } // For OpenAI models which are just strings
+				: models[modelId] // For other models that have full info objects
+			const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
 			setApiConfiguration(apiConfig)
 			onUpdateApiConfig(apiConfig)
 			setValue(modelId)
@@ -68,8 +81,14 @@ export const ModelPicker = ({
 	)
 
 	const debouncedRefreshModels = useMemo(
-		() => debounce(() => vscode.postMessage({ type: refreshMessageType }), 50),
-		[refreshMessageType],
+		() =>
+			debounce(() => {
+				const message = refreshValues
+					? { type: refreshMessageType, values: refreshValues }
+					: { type: refreshMessageType }
+				vscode.postMessage(message)
+			}, 50),
+		[refreshMessageType, refreshValues],
 	)
 
 	useMount(() => {

+ 17 - 208
webview-ui/src/components/settings/OpenAiModelPicker.tsx

@@ -1,217 +1,26 @@
-import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import debounce from "debounce"
-import { Fzf } from "fzf"
-import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
-
+import React from "react"
 import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
-import { highlightFzfMatch } from "../../utils/highlight"
-import { DropdownWrapper, DropdownList, DropdownItem } from "./styles"
+import { ModelPicker } from "./ModelPicker"
 
 const OpenAiModelPicker: React.FC = () => {
-	const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
-	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openAiModelId || "")
-	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
-	const [selectedIndex, setSelectedIndex] = useState(-1)
-	const dropdownRef = useRef<HTMLDivElement>(null)
-	const itemRefs = useRef<(HTMLDivElement | null)[]>([])
-	const dropdownListRef = useRef<HTMLDivElement>(null)
-
-	const handleModelChange = (newModelId: string) => {
-		// could be setting invalid model id/undefined info but validation will catch it
-		const apiConfig = {
-			...apiConfiguration,
-			openAiModelId: newModelId,
-		}
-
-		setApiConfiguration(apiConfig)
-		onUpdateApiConfig(apiConfig)
-		setSearchTerm(newModelId)
-	}
-
-	useEffect(() => {
-		if (apiConfiguration?.openAiModelId && apiConfiguration?.openAiModelId !== searchTerm) {
-			setSearchTerm(apiConfiguration?.openAiModelId)
-		}
-	}, [apiConfiguration, searchTerm])
-
-	const debouncedRefreshModels = useMemo(
-		() =>
-			debounce((baseUrl: string, apiKey: string) => {
-				vscode.postMessage({
-					type: "refreshOpenAiModels",
-					values: {
-						baseUrl,
-						apiKey,
-					},
-				})
-			}, 50),
-		[],
-	)
-
-	useEffect(() => {
-		if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
-			return
-		}
-
-		debouncedRefreshModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
-
-		// Cleanup debounced function
-		return () => {
-			debouncedRefreshModels.clear()
-		}
-	}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, debouncedRefreshModels])
-
-	useEffect(() => {
-		const handleClickOutside = (event: MouseEvent) => {
-			if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
-				setIsDropdownVisible(false)
-			}
-		}
-
-		document.addEventListener("mousedown", handleClickOutside)
-		return () => {
-			document.removeEventListener("mousedown", handleClickOutside)
-		}
-	}, [])
-
-	const modelIds = useMemo(() => {
-		return openAiModels.sort((a, b) => a.localeCompare(b))
-	}, [openAiModels])
-
-	const searchableItems = useMemo(() => {
-		return modelIds.map((id) => ({
-			id,
-			html: id,
-		}))
-	}, [modelIds])
-
-	const fzf = useMemo(() => {
-		return new Fzf(searchableItems, {
-			selector: (item) => item.html,
-		})
-	}, [searchableItems])
-
-	const modelSearchResults = useMemo(() => {
-		if (!searchTerm) return searchableItems
-
-		const searchResults = fzf.find(searchTerm)
-		return searchResults.map((result) => ({
-			...result.item,
-			html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight"),
-		}))
-	}, [searchableItems, searchTerm, fzf])
-
-	const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
-		if (!isDropdownVisible) return
-
-		switch (event.key) {
-			case "ArrowDown":
-				event.preventDefault()
-				setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
-				break
-			case "ArrowUp":
-				event.preventDefault()
-				setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
-				break
-			case "Enter":
-				event.preventDefault()
-				if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
-					handleModelChange(modelSearchResults[selectedIndex].id)
-					setIsDropdownVisible(false)
-				}
-				break
-			case "Escape":
-				setIsDropdownVisible(false)
-				setSelectedIndex(-1)
-				break
-		}
-	}
-
-	useEffect(() => {
-		setSelectedIndex(-1)
-		if (dropdownListRef.current) {
-			dropdownListRef.current.scrollTop = 0
-		}
-	}, [searchTerm])
-
-	useEffect(() => {
-		if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
-			itemRefs.current[selectedIndex]?.scrollIntoView({
-				block: "nearest",
-				behavior: "smooth",
-			})
-		}
-	}, [selectedIndex])
+	const { apiConfiguration } = useExtensionState()
 
 	return (
-		<>
-			<style>
-				{`
-				.model-item-highlight {
-					background-color: var(--vscode-editor-findMatchHighlightBackground);
-					color: inherit;
-				}
-				`}
-			</style>
-			<div>
-				<DropdownWrapper ref={dropdownRef}>
-					<VSCodeTextField
-						id="model-search"
-						placeholder="Search and select a model..."
-						value={searchTerm}
-						onInput={(e) => {
-							handleModelChange((e.target as HTMLInputElement)?.value)
-							setIsDropdownVisible(true)
-						}}
-						onFocus={() => setIsDropdownVisible(true)}
-						onKeyDown={handleKeyDown}
-						style={{ width: "100%", zIndex: OPENAI_MODEL_PICKER_Z_INDEX, position: "relative" }}>
-						{searchTerm && (
-							<div
-								className="input-icon-button codicon codicon-close"
-								aria-label="Clear search"
-								onClick={() => {
-									handleModelChange("")
-									setIsDropdownVisible(true)
-								}}
-								slot="end"
-								style={{
-									display: "flex",
-									justifyContent: "center",
-									alignItems: "center",
-									height: "100%",
-								}}
-							/>
-						)}
-					</VSCodeTextField>
-					{isDropdownVisible && (
-						<DropdownList ref={dropdownListRef} $zIndex={OPENAI_MODEL_PICKER_Z_INDEX - 1}>
-							{modelSearchResults.map((item, index) => (
-								<DropdownItem
-									$selected={index === selectedIndex}
-									key={item.id}
-									ref={(el) => (itemRefs.current[index] = el)}
-									onMouseEnter={() => setSelectedIndex(index)}
-									onClick={() => {
-										handleModelChange(item.id)
-										setIsDropdownVisible(false)
-									}}
-									dangerouslySetInnerHTML={{
-										__html: item.html,
-									}}
-								/>
-							))}
-						</DropdownList>
-					)}
-				</DropdownWrapper>
-			</div>
-		</>
+		<ModelPicker
+			defaultModelId={apiConfiguration?.openAiModelId || ""}
+			modelsKey="openAiModels"
+			configKey="openAiModelId"
+			infoKey="openAiModelInfo"
+			refreshMessageType="refreshOpenAiModels"
+			refreshValues={{
+				baseUrl: apiConfiguration?.openAiBaseUrl,
+				apiKey: apiConfiguration?.openAiApiKey,
+			}}
+			serviceName="OpenAI"
+			serviceUrl="https://platform.openai.com"
+			recommendedModel="gpt-4-turbo-preview"
+		/>
 	)
 }
 
 export default OpenAiModelPicker
-
-// Dropdown
-
-export const OPENAI_MODEL_PICKER_Z_INDEX = 1_000

+ 19 - 12
webview-ui/src/components/settings/RequestyModelPicker.tsx

@@ -1,15 +1,22 @@
 import { ModelPicker } from "./ModelPicker"
 import { requestyDefaultModelId } from "../../../../src/shared/api"
+import { useExtensionState } from "@/context/ExtensionStateContext"
 
-export const RequestyModelPicker = () => (
-	<ModelPicker
-		defaultModelId={requestyDefaultModelId}
-		modelsKey="requestyModels"
-		configKey="requestyModelId"
-		infoKey="requestyModelInfo"
-		refreshMessageType="refreshRequestyModels"
-		serviceName="Requesty"
-		serviceUrl="https://requesty.ai"
-		recommendedModel="anthropic/claude-3-5-sonnet-latest"
-	/>
-)
+export const RequestyModelPicker = () => {
+	const { apiConfiguration } = useExtensionState()
+	return (
+		<ModelPicker
+			defaultModelId={requestyDefaultModelId}
+			modelsKey="requestyModels"
+			configKey="requestyModelId"
+			infoKey="requestyModelInfo"
+			refreshMessageType="refreshRequestyModels"
+			refreshValues={{
+				apiKey: apiConfiguration?.requestyApiKey,
+			}}
+			serviceName="Requesty"
+			serviceUrl="https://requesty.ai"
+			recommendedModel="anthropic/claude-3-5-sonnet-latest"
+		/>
+	)
+}