|
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" |
6 | 2 | 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" |
10 | 4 |
|
11 | 5 | 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() |
146 | 7 |
|
147 | 8 | 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 | + /> |
210 | 23 | ) |
211 | 24 | } |
212 | 25 |
|
213 | 26 | export default OpenAiModelPicker |
214 | | - |
215 | | -// Dropdown |
216 | | - |
217 | | -export const OPENAI_MODEL_PICKER_Z_INDEX = 1_000 |
0 commit comments