Skip to content

Commit 26a315b

Browse files
committed
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
1 parent b194408 commit 26a315b

File tree

3 files changed

+63
-228
lines changed

3 files changed

+63
-228
lines changed

webview-ui/src/components/settings/ModelPicker.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@ import { ModelInfoView } from "./ModelInfoView"
2525

2626
interface ModelPickerProps {
2727
defaultModelId: string
28-
modelsKey: "glamaModels" | "openRouterModels" | "unboundModels" | "requestyModels"
29-
configKey: "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId"
30-
infoKey: "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo"
31-
refreshMessageType: "refreshGlamaModels" | "refreshOpenRouterModels" | "refreshUnboundModels" | "refreshRequestyModels"
28+
modelsKey: "glamaModels" | "openRouterModels" | "unboundModels" | "requestyModels" | "openAiModels"
29+
configKey: "glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId" | "openAiModelId"
30+
infoKey: "glamaModelInfo" | "openRouterModelInfo" | "unboundModelInfo" | "requestyModelInfo" | "openAiModelInfo"
31+
refreshMessageType:
32+
| "refreshGlamaModels"
33+
| "refreshOpenRouterModels"
34+
| "refreshUnboundModels"
35+
| "refreshRequestyModels"
36+
| "refreshOpenAiModels"
37+
refreshValues?: Record<string, any>
3238
serviceName: string
3339
serviceUrl: string
3440
recommendedModel: string
@@ -40,6 +46,7 @@ export const ModelPicker = ({
4046
configKey,
4147
infoKey,
4248
refreshMessageType,
49+
refreshValues,
4350
serviceName,
4451
serviceUrl,
4552
recommendedModel,
@@ -49,7 +56,10 @@ export const ModelPicker = ({
4956
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
5057

5158
const { apiConfiguration, setApiConfiguration, [modelsKey]: models, onUpdateApiConfig } = useExtensionState()
52-
const modelIds = useMemo(() => Object.keys(models).sort((a, b) => a.localeCompare(b)), [models])
59+
const modelIds = useMemo(
60+
() => (Array.isArray(models) ? models : Object.keys(models)).sort((a, b) => a.localeCompare(b)),
61+
[models],
62+
)
5363

5464
const { selectedModelId, selectedModelInfo } = useMemo(
5565
() => normalizeApiConfiguration(apiConfiguration),
@@ -58,7 +68,10 @@ export const ModelPicker = ({
5868

5969
const onSelect = useCallback(
6070
(modelId: string) => {
61-
const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: models[modelId] }
71+
const modelInfo = Array.isArray(models)
72+
? { id: modelId } // For OpenAI models which are just strings
73+
: models[modelId] // For other models that have full info objects
74+
const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
6275
setApiConfiguration(apiConfig)
6376
onUpdateApiConfig(apiConfig)
6477
setValue(modelId)
@@ -68,8 +81,14 @@ export const ModelPicker = ({
6881
)
6982

7083
const debouncedRefreshModels = useMemo(
71-
() => debounce(() => vscode.postMessage({ type: refreshMessageType }), 50),
72-
[refreshMessageType],
84+
() =>
85+
debounce(() => {
86+
const message = refreshValues
87+
? { type: refreshMessageType, values: refreshValues }
88+
: { type: refreshMessageType }
89+
vscode.postMessage(message)
90+
}, 50),
91+
[refreshMessageType, refreshValues],
7392
)
7493

7594
useMount(() => {
Lines changed: 17 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,217 +1,26 @@
1-
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
2-
import debounce from "debounce"
3-
import { Fzf } from "fzf"
4-
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
5-
1+
import React from "react"
62
import { useExtensionState } from "../../context/ExtensionStateContext"
7-
import { vscode } from "../../utils/vscode"
8-
import { highlightFzfMatch } from "../../utils/highlight"
9-
import { DropdownWrapper, DropdownList, DropdownItem } from "./styles"
3+
import { ModelPicker } from "./ModelPicker"
104

115
const OpenAiModelPicker: React.FC = () => {
12-
const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
13-
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openAiModelId || "")
14-
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
15-
const [selectedIndex, setSelectedIndex] = useState(-1)
16-
const dropdownRef = useRef<HTMLDivElement>(null)
17-
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
18-
const dropdownListRef = useRef<HTMLDivElement>(null)
19-
20-
const handleModelChange = (newModelId: string) => {
21-
// could be setting invalid model id/undefined info but validation will catch it
22-
const apiConfig = {
23-
...apiConfiguration,
24-
openAiModelId: newModelId,
25-
}
26-
27-
setApiConfiguration(apiConfig)
28-
onUpdateApiConfig(apiConfig)
29-
setSearchTerm(newModelId)
30-
}
31-
32-
useEffect(() => {
33-
if (apiConfiguration?.openAiModelId && apiConfiguration?.openAiModelId !== searchTerm) {
34-
setSearchTerm(apiConfiguration?.openAiModelId)
35-
}
36-
}, [apiConfiguration, searchTerm])
37-
38-
const debouncedRefreshModels = useMemo(
39-
() =>
40-
debounce((baseUrl: string, apiKey: string) => {
41-
vscode.postMessage({
42-
type: "refreshOpenAiModels",
43-
values: {
44-
baseUrl,
45-
apiKey,
46-
},
47-
})
48-
}, 50),
49-
[],
50-
)
51-
52-
useEffect(() => {
53-
if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
54-
return
55-
}
56-
57-
debouncedRefreshModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
58-
59-
// Cleanup debounced function
60-
return () => {
61-
debouncedRefreshModels.clear()
62-
}
63-
}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, debouncedRefreshModels])
64-
65-
useEffect(() => {
66-
const handleClickOutside = (event: MouseEvent) => {
67-
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
68-
setIsDropdownVisible(false)
69-
}
70-
}
71-
72-
document.addEventListener("mousedown", handleClickOutside)
73-
return () => {
74-
document.removeEventListener("mousedown", handleClickOutside)
75-
}
76-
}, [])
77-
78-
const modelIds = useMemo(() => {
79-
return openAiModels.sort((a, b) => a.localeCompare(b))
80-
}, [openAiModels])
81-
82-
const searchableItems = useMemo(() => {
83-
return modelIds.map((id) => ({
84-
id,
85-
html: id,
86-
}))
87-
}, [modelIds])
88-
89-
const fzf = useMemo(() => {
90-
return new Fzf(searchableItems, {
91-
selector: (item) => item.html,
92-
})
93-
}, [searchableItems])
94-
95-
const modelSearchResults = useMemo(() => {
96-
if (!searchTerm) return searchableItems
97-
98-
const searchResults = fzf.find(searchTerm)
99-
return searchResults.map((result) => ({
100-
...result.item,
101-
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight"),
102-
}))
103-
}, [searchableItems, searchTerm, fzf])
104-
105-
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
106-
if (!isDropdownVisible) return
107-
108-
switch (event.key) {
109-
case "ArrowDown":
110-
event.preventDefault()
111-
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
112-
break
113-
case "ArrowUp":
114-
event.preventDefault()
115-
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
116-
break
117-
case "Enter":
118-
event.preventDefault()
119-
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
120-
handleModelChange(modelSearchResults[selectedIndex].id)
121-
setIsDropdownVisible(false)
122-
}
123-
break
124-
case "Escape":
125-
setIsDropdownVisible(false)
126-
setSelectedIndex(-1)
127-
break
128-
}
129-
}
130-
131-
useEffect(() => {
132-
setSelectedIndex(-1)
133-
if (dropdownListRef.current) {
134-
dropdownListRef.current.scrollTop = 0
135-
}
136-
}, [searchTerm])
137-
138-
useEffect(() => {
139-
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
140-
itemRefs.current[selectedIndex]?.scrollIntoView({
141-
block: "nearest",
142-
behavior: "smooth",
143-
})
144-
}
145-
}, [selectedIndex])
6+
const { apiConfiguration } = useExtensionState()
1467

1478
return (
148-
<>
149-
<style>
150-
{`
151-
.model-item-highlight {
152-
background-color: var(--vscode-editor-findMatchHighlightBackground);
153-
color: inherit;
154-
}
155-
`}
156-
</style>
157-
<div>
158-
<DropdownWrapper ref={dropdownRef}>
159-
<VSCodeTextField
160-
id="model-search"
161-
placeholder="Search and select a model..."
162-
value={searchTerm}
163-
onInput={(e) => {
164-
handleModelChange((e.target as HTMLInputElement)?.value)
165-
setIsDropdownVisible(true)
166-
}}
167-
onFocus={() => setIsDropdownVisible(true)}
168-
onKeyDown={handleKeyDown}
169-
style={{ width: "100%", zIndex: OPENAI_MODEL_PICKER_Z_INDEX, position: "relative" }}>
170-
{searchTerm && (
171-
<div
172-
className="input-icon-button codicon codicon-close"
173-
aria-label="Clear search"
174-
onClick={() => {
175-
handleModelChange("")
176-
setIsDropdownVisible(true)
177-
}}
178-
slot="end"
179-
style={{
180-
display: "flex",
181-
justifyContent: "center",
182-
alignItems: "center",
183-
height: "100%",
184-
}}
185-
/>
186-
)}
187-
</VSCodeTextField>
188-
{isDropdownVisible && (
189-
<DropdownList ref={dropdownListRef} $zIndex={OPENAI_MODEL_PICKER_Z_INDEX - 1}>
190-
{modelSearchResults.map((item, index) => (
191-
<DropdownItem
192-
$selected={index === selectedIndex}
193-
key={item.id}
194-
ref={(el) => (itemRefs.current[index] = el)}
195-
onMouseEnter={() => setSelectedIndex(index)}
196-
onClick={() => {
197-
handleModelChange(item.id)
198-
setIsDropdownVisible(false)
199-
}}
200-
dangerouslySetInnerHTML={{
201-
__html: item.html,
202-
}}
203-
/>
204-
))}
205-
</DropdownList>
206-
)}
207-
</DropdownWrapper>
208-
</div>
209-
</>
9+
<ModelPicker
10+
defaultModelId={apiConfiguration?.openAiModelId || ""}
11+
modelsKey="openAiModels"
12+
configKey="openAiModelId"
13+
infoKey="openAiModelInfo"
14+
refreshMessageType="refreshOpenAiModels"
15+
refreshValues={{
16+
baseUrl: apiConfiguration?.openAiBaseUrl,
17+
apiKey: apiConfiguration?.openAiApiKey,
18+
}}
19+
serviceName="OpenAI"
20+
serviceUrl="https://platform.openai.com"
21+
recommendedModel="gpt-4-turbo-preview"
22+
/>
21023
)
21124
}
21225

21326
export default OpenAiModelPicker
214-
215-
// Dropdown
216-
217-
export const OPENAI_MODEL_PICKER_Z_INDEX = 1_000
Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { ModelPicker } from "./ModelPicker"
22
import { requestyDefaultModelId } from "../../../../src/shared/api"
3+
import { useExtensionState } from "@/context/ExtensionStateContext"
34

4-
export const RequestyModelPicker = () => (
5-
<ModelPicker
6-
defaultModelId={requestyDefaultModelId}
7-
modelsKey="requestyModels"
8-
configKey="requestyModelId"
9-
infoKey="requestyModelInfo"
10-
refreshMessageType="refreshRequestyModels"
11-
serviceName="Requesty"
12-
serviceUrl="https://requesty.ai"
13-
recommendedModel="anthropic/claude-3-5-sonnet-latest"
14-
/>
15-
)
5+
export const RequestyModelPicker = () => {
6+
const { apiConfiguration } = useExtensionState()
7+
return (
8+
<ModelPicker
9+
defaultModelId={requestyDefaultModelId}
10+
modelsKey="requestyModels"
11+
configKey="requestyModelId"
12+
infoKey="requestyModelInfo"
13+
refreshMessageType="refreshRequestyModels"
14+
refreshValues={{
15+
apiKey: apiConfiguration?.requestyApiKey,
16+
}}
17+
serviceName="Requesty"
18+
serviceUrl="https://requesty.ai"
19+
recommendedModel="anthropic/claude-3-5-sonnet-latest"
20+
/>
21+
)
22+
}

0 commit comments

Comments
 (0)