Browse Source

using ModelPicker for Requesty Model Picker

sam hoang 11 months ago
parent
commit
d6e55afb51

+ 2 - 1
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

+ 1 - 1
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

+ 4 - 4
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

+ 12 - 420
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<HTMLDivElement>(null)
-	const itemRefs = useRef<(HTMLDivElement | null)[]>([])
-	const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
-	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,
-			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<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
-		}
-	}
-
-	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 (
-		<>
-			<style>
-				{`
-				.model-item-highlight {
-					background-color: var(--vscode-editor-findMatchHighlightBackground);
-					color: inherit;
-				}
-				`}
-			</style>
-			<div>
-				<label htmlFor="model-search">
-					<span style={{ fontWeight: 500 }}>Model</span>
-				</label>
-				<DropdownWrapper ref={dropdownRef}>
-					<VSCodeTextField
-						id="model-search"
-						placeholder="Search and select a model..."
-						value={searchTerm}
-						onInput={(e) => {
-							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 && (
-							<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}>
-							{modelSearchResults.map((item, index) => (
-								<DropdownItem
-									key={item.id}
-									ref={(el) => (itemRefs.current[index] = el)}
-									isSelected={index === selectedIndex}
-									onMouseEnter={() => setSelectedIndex(index)}
-									onClick={() => {
-										handleModelChange(item.id)
-										setIsDropdownVisible(false)
-									}}
-									dangerouslySetInnerHTML={{
-										__html: item.html,
-									}}
-								/>
-							))}
-						</DropdownList>
-					)}
-				</DropdownWrapper>
-			</div>
-
-			{hasInfo ? (
-				<ModelInfoView
-					selectedModelId={selectedModelId}
-					modelInfo={selectedModelInfo}
-					isDescriptionExpanded={isDescriptionExpanded}
-					setIsDescriptionExpanded={setIsDescriptionExpanded}
-				/>
-			) : (
-				<p
-					style={{
-						fontSize: "12px",
-						marginTop: 0,
-						color: "var(--vscode-descriptionForeground)",
-					}}>
-					The extension automatically fetches the latest list of models available on{" "}
-					<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://requesty.ai/models">
-						Requesty.
-					</VSCodeLink>
-					If you're unsure which model to choose, Roo Code works best with{" "}
-					<VSCodeLink
-						style={{ display: "inline", fontSize: "inherit" }}
-						onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
-						anthropic/claude-3.5-sonnet.
-					</VSCodeLink>
-					You can also try searching "free" for no-cost options currently available.
-				</p>
-			)}
-		</>
-	)
-}
-
-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<HTMLDivElement>(null)
-		const textRef = useRef<HTMLDivElement>(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 (
-			<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
-				<div
-					ref={textContainerRef}
-					style={{
-						overflowY: isExpanded ? "auto" : "hidden",
-						position: "relative",
-						wordBreak: "break-word",
-						overflowWrap: "anywhere",
-					}}>
-					<div
-						ref={textRef}
-						style={{
-							display: "-webkit-box",
-							WebkitLineClamp: isExpanded ? "unset" : 3,
-							WebkitBoxOrient: "vertical",
-							overflow: "hidden",
-						}}>
-						{reactContent}
-					</div>
-					{!isExpanded && showSeeMore && (
-						<div
-							style={{
-								position: "absolute",
-								right: 0,
-								bottom: 0,
-								display: "flex",
-								alignItems: "center",
-							}}>
-							<div
-								style={{
-									width: 30,
-									height: "1.2em",
-									background:
-										"linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
-								}}
-							/>
-							<VSCodeLink
-								style={{
-									fontSize: "inherit",
-									paddingRight: 0,
-									paddingLeft: 3,
-									backgroundColor: "var(--vscode-sideBar-background)",
-								}}
-								onClick={() => setIsExpanded(true)}>
-								See more
-							</VSCodeLink>
-						</div>
-					)}
-				</div>
-			</StyledMarkdown>
-		)
-	},
+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"
+	/>
 )