From d6e55afb518cc7c97923e2cfdc339b4bf50354a3 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Mon, 10 Feb 2025 20:11:29 +0700 Subject: [PATCH] using ModelPicker for Requesty Model Picker --- src/shared/api.ts | 3 +- .../src/components/settings/ApiOptions.tsx | 2 +- .../src/components/settings/ModelPicker.tsx | 8 +- .../settings/RequestyModelPicker.tsx | 432 +----------------- 4 files changed, 19 insertions(+), 426 deletions(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index f0afb333a6..93ebaed9bc 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -343,7 +343,6 @@ export const bedrockModels = { // Glama // https://glama.ai/models export const glamaDefaultModelId = "anthropic/claude-3-5-sonnet" -export const requestyDefaultModelId = "anthropic/claude-3-5-sonnet" export const glamaDefaultModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -357,6 +356,7 @@ export const glamaDefaultModelInfo: ModelInfo = { description: "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", } + export const requestyDefaultModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -370,6 +370,7 @@ export const requestyDefaultModelInfo: ModelInfo = { description: "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", } +export const requestyDefaultModelId = "anthropic/claude-3-5-sonnet" // OpenRouter // https://openrouter.ai/models?order=newest&supported_parameters=tools diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 1d8ca04b77..2848fb7fd5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -42,7 +42,7 @@ import { GlamaModelPicker } from "./GlamaModelPicker" import { UnboundModelPicker } from "./UnboundModelPicker" import { ModelInfoView } from "./ModelInfoView" import { DROPDOWN_Z_INDEX } from "./styles" -import RequestyModelPicker from "./RequestyModelPicker" +import { RequestyModelPicker } from "./RequestyModelPicker" interface ApiOptionsProps { apiErrorMessage?: string diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 0b609466bb..f45f857d9d 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -25,10 +25,10 @@ import { ModelInfoView } from "./ModelInfoView" interface ModelPickerProps { defaultModelId: string - modelsKey: "glamaModels" | "openRouterModels" | "unboundModels" - configKey: "glamaModelId" | "openRouterModelId" | "unboundModelId" - infoKey: "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" - refreshMessageType: "refreshGlamaModels" | "refreshOpenRouterModels" | "refreshUnboundModels" + modelsKey: "glamaModels" | "openRouterModels" | "unboundModels" | "requestyModels" + configKey: "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId" + infoKey: "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo" + refreshMessageType: "refreshGlamaModels" | "refreshOpenRouterModels" | "refreshUnboundModels" | "refreshRequestyModels" serviceName: string serviceUrl: string recommendedModel: string diff --git a/webview-ui/src/components/settings/RequestyModelPicker.tsx b/webview-ui/src/components/settings/RequestyModelPicker.tsx index 899ffe62a8..bdc2db0744 100644 --- a/webview-ui/src/components/settings/RequestyModelPicker.tsx +++ b/webview-ui/src/components/settings/RequestyModelPicker.tsx @@ -1,423 +1,15 @@ -import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import debounce from "debounce" -import { Fzf } from "fzf" -import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" -import { useRemark } from "react-remark" -import styled from "styled-components" +import { ModelPicker } from "./ModelPicker" import { requestyDefaultModelId } from "../../../../src/shared/api" -import { useExtensionState } from "../../context/ExtensionStateContext" -import { vscode } from "../../utils/vscode" -import { highlightFzfMatch } from "../../utils/highlight" -import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions" -const RequestyModelPicker: React.FC = () => { - const { apiConfiguration, setApiConfiguration, requestyModels, onUpdateApiConfig } = useExtensionState() - const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId) - const [isDropdownVisible, setIsDropdownVisible] = useState(false) - const [selectedIndex, setSelectedIndex] = useState(-1) - const dropdownRef = useRef(null) - const itemRefs = useRef<(HTMLDivElement | null)[]>([]) - const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const dropdownListRef = useRef(null) - - const handleModelChange = (newModelId: string) => { - // could be setting invalid model id/undefined info but validation will catch it - const apiConfig = { - ...apiConfiguration, - requestyModelId: newModelId, - requestyModelInfo: requestyModels[newModelId], - } - setApiConfiguration(apiConfig) - onUpdateApiConfig(apiConfig) - - setSearchTerm(newModelId) - } - - const { selectedModelId, selectedModelInfo } = useMemo(() => { - return normalizeApiConfiguration(apiConfiguration) - }, [apiConfiguration]) - - useEffect(() => { - if (apiConfiguration?.requestyModelId && apiConfiguration?.requestyModelId !== searchTerm) { - setSearchTerm(apiConfiguration?.requestyModelId) - } - }, [apiConfiguration, searchTerm]) - - const debouncedRefreshModels = useMemo( - () => - debounce((apiKey: string) => { - vscode.postMessage({ - type: "refreshRequestyModels", - values: { - apiKey, - }, - }) - }, 50), - [], - ) - - useEffect(() => { - if (!apiConfiguration?.requestyApiKey) { - return - } - - debouncedRefreshModels(apiConfiguration.requestyApiKey) - - // Cleanup debounced function - return () => { - debouncedRefreshModels.clear() - } - }, [apiConfiguration?.requestyApiKey, 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 Object.keys(requestyModels).sort((a, b) => a.localeCompare(b)) - }, [requestyModels]) - - 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) => { - 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) - if (dropdownListRef.current) { - dropdownListRef.current.scrollTop = 0 - } - }, [searchTerm]) - - useEffect(() => { - if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) { - itemRefs.current[selectedIndex]?.scrollIntoView({ - block: "nearest", - behavior: "smooth", - }) - } - }, [selectedIndex]) - - return ( - <> - -
- - - { - handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase()) - setIsDropdownVisible(true) - }} - onFocus={() => setIsDropdownVisible(true)} - onKeyDown={handleKeyDown} - style={{ width: "100%", zIndex: GLAMA_MODEL_PICKER_Z_INDEX, position: "relative" }}> - {searchTerm && ( -
{ - handleModelChange("") - setIsDropdownVisible(true) - }} - slot="end" - style={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - height: "100%", - }} - /> - )} - - {isDropdownVisible && ( - - {modelSearchResults.map((item, index) => ( - (itemRefs.current[index] = el)} - isSelected={index === selectedIndex} - onMouseEnter={() => setSelectedIndex(index)} - onClick={() => { - handleModelChange(item.id) - setIsDropdownVisible(false) - }} - dangerouslySetInnerHTML={{ - __html: item.html, - }} - /> - ))} - - )} - -
- - {hasInfo ? ( - - ) : ( -

- The extension automatically fetches the latest list of models available on{" "} - - Requesty. - - If you're unsure which model to choose, Roo Code works best with{" "} - handleModelChange("anthropic/claude-3.5-sonnet")}> - anthropic/claude-3.5-sonnet. - - You can also try searching "free" for no-cost options currently available. -

- )} - - ) -} - -export default RequestyModelPicker - -// Dropdown - -const DropdownWrapper = styled.div` - position: relative; - width: 100%; -` - -export const GLAMA_MODEL_PICKER_Z_INDEX = 1_000 - -const DropdownList = styled.div` - position: absolute; - top: calc(100% - 3px); - left: 0; - width: calc(100% - 2px); - max-height: 200px; - overflow-y: auto; - background-color: var(--vscode-dropdown-background); - border: 1px solid var(--vscode-list-activeSelectionBackground); - z-index: ${GLAMA_MODEL_PICKER_Z_INDEX - 1}; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -` - -const DropdownItem = styled.div<{ isSelected: boolean }>` - padding: 5px 10px; - cursor: pointer; - word-break: break-all; - white-space: normal; - - background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")}; - - &:hover { - background-color: var(--vscode-list-activeSelectionBackground); - } -` - -// Markdown - -const StyledMarkdown = styled.div` - font-family: - var(--vscode-font-family), - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - Oxygen, - Ubuntu, - Cantarell, - "Open Sans", - "Helvetica Neue", - sans-serif; - font-size: 12px; - color: var(--vscode-descriptionForeground); - - p, - li, - ol, - ul { - line-height: 1.25; - margin: 0; - } - - ol, - ul { - padding-left: 1.5em; - margin-left: 0; - } - - p { - white-space: pre-wrap; - } - - a { - text-decoration: none; - } - a { - &:hover { - text-decoration: underline; - } - } -` - -export const ModelDescriptionMarkdown = memo( - ({ - markdown, - key, - isExpanded, - setIsExpanded, - }: { - markdown?: string - key: string - isExpanded: boolean - setIsExpanded: (isExpanded: boolean) => void - }) => { - const [reactContent, setMarkdown] = useRemark() - const [showSeeMore, setShowSeeMore] = useState(false) - const textContainerRef = useRef(null) - const textRef = useRef(null) - - useEffect(() => { - setMarkdown(markdown || "") - }, [markdown, setMarkdown]) - - useEffect(() => { - if (textRef.current && textContainerRef.current) { - const { scrollHeight } = textRef.current - const { clientHeight } = textContainerRef.current - const isOverflowing = scrollHeight > clientHeight - setShowSeeMore(isOverflowing) - } - }, [reactContent, setIsExpanded]) - - return ( - -
-
- {reactContent} -
- {!isExpanded && showSeeMore && ( -
-
- setIsExpanded(true)}> - See more - -
- )} -
- - ) - }, +export const RequestyModelPicker = () => ( + )