mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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
This commit is contained in:
parent
b194408d03
commit
26a315b946
3 changed files with 63 additions and 228 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue