diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index dba15ee..aec9ca6 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -36,9 +36,10 @@ import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterMod interface ApiOptionsProps { showModelOptions: boolean apiErrorMessage?: string + modelIdErrorMessage?: string } -const ApiOptions = ({ showModelOptions, apiErrorMessage }: ApiOptionsProps) => { +const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => { const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState() const [ollamaModels, setOllamaModels] = useState([]) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) @@ -544,6 +545,17 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage }: ApiOptionsProps) => { )} + + {modelIdErrorMessage && ( +

+ {modelIdErrorMessage} +

+ )} ) } diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx index 7f018eb..fb51f5b 100644 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx @@ -1,6 +1,6 @@ -import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import Fuse from "fuse.js" -import React, { memo, useEffect, useMemo, useRef, useState } from "react" +import React, { memo, useEffect, useMemo, useRef, useState, KeyboardEvent } from "react" import { useRemark } from "react-remark" import { useMount } from "react-use" import styled from "styled-components" @@ -8,12 +8,15 @@ import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions" import { highlight } from "../history/HistoryView" +import { openRouterDefaultModelId } from "../../../../src/shared/api" const OpenRouterModelPicker: React.FC = () => { const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState() - const [searchTerm, setSearchTerm] = useState("") + const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId) const [isDropdownVisible, setIsDropdownVisible] = useState(false) + const [selectedIndex, setSelectedIndex] = useState(-1) const dropdownRef = useRef(null) + const itemRefs = useRef<(HTMLDivElement | null)[]>([]) const handleModelChange = (newModelId: string) => { setApiConfiguration({ @@ -22,7 +25,6 @@ const OpenRouterModelPicker: React.FC = () => { openRouterModelInfo: openRouterModels[newModelId], }) setSearchTerm(newModelId) - setIsDropdownVisible(false) } const { selectedModelId, selectedModelInfo } = useMemo(() => { @@ -46,15 +48,17 @@ const OpenRouterModelPicker: React.FC = () => { } }, []) - const searchableItems = useMemo(() => { - return Object.keys(openRouterModels) - .sort((a, b) => a.localeCompare(b)) - .map((id) => ({ - id, - html: id, - })) + const modelIds = useMemo(() => { + return Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b)) }, [openRouterModels]) + const searchableItems = useMemo(() => { + return modelIds.map((id) => ({ + id, + html: id, + })) + }, [modelIds]) + const fuse = useMemo(() => { return new Fuse(searchableItems, { keys: ["html"], // highlight function will update this @@ -75,6 +79,49 @@ const OpenRouterModelPicker: React.FC = () => { return results }, [searchableItems, searchTerm, fuse]) + const handleKeyDown = (event: KeyboardEvent) => { + 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 + } + } + + const hasInfo = useMemo(() => { + return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase()) + }, [modelIds, searchTerm]) + + useEffect(() => { + setSelectedIndex(-1) + }, [searchTerm]) + + useEffect(() => { + if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) { + itemRefs.current[selectedIndex]?.scrollIntoView({ + block: "nearest", + behavior: "smooth", + }) + } + }, [selectedIndex]) + return ( <>