mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge pull request #1122 from System233/patch-model-picker
fix: ModelID cannot be saved and refactor ModelPicker
This commit is contained in:
commit
3860abf855
19 changed files with 1212 additions and 503 deletions
6
webview-ui/src/__mocks__/lucide-react.ts
Normal file
6
webview-ui/src/__mocks__/lucide-react.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import React from "react"
|
||||
|
||||
export const Check = () => React.createElement("div")
|
||||
export const ChevronsUpDown = () => React.createElement("div")
|
||||
export const Loader = () => React.createElement("div")
|
||||
export const X = () => React.createElement("div")
|
||||
|
|
@ -8,6 +8,9 @@ export const Dropdown = ({ children, value, onChange }: any) =>
|
|||
|
||||
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)
|
||||
|
||||
export const Button = ({ children, ...props }: any) =>
|
||||
React.createElement("div", { "data-testid": "mock-button", ...props }, children)
|
||||
|
||||
export type DropdownOption = {
|
||||
label: string
|
||||
value: string
|
||||
|
|
|
|||
16
webview-ui/src/components/settings/ApiErrorMessage.tsx
Normal file
16
webview-ui/src/components/settings/ApiErrorMessage.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react"
|
||||
|
||||
interface ApiErrorMessageProps {
|
||||
errorMessage: string | undefined
|
||||
children?: React.ReactNode
|
||||
}
|
||||
const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
|
||||
return (
|
||||
<div className="text-vscode-errorForeground text-sm">
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{errorMessage}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ApiErrorMessage
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback, useMemo, useState } from "react"
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useDebounce, useEvent } from "react-use"
|
||||
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
|
||||
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
|
@ -39,35 +39,47 @@ import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
|||
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import { OpenRouterModelPicker } from "./OpenRouterModelPicker"
|
||||
import OpenAiModelPicker from "./OpenAiModelPicker"
|
||||
import { GlamaModelPicker } from "./GlamaModelPicker"
|
||||
import { UnboundModelPicker } from "./UnboundModelPicker"
|
||||
import { ModelInfoView } from "./ModelInfoView"
|
||||
import { DROPDOWN_Z_INDEX } from "./styles"
|
||||
import { RequestyModelPicker } from "./RequestyModelPicker"
|
||||
import { ModelPicker } from "./ModelPicker"
|
||||
import { TemperatureControl } from "./TemperatureControl"
|
||||
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
|
||||
import ApiErrorMessage from "./ApiErrorMessage"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
uriScheme: string | undefined
|
||||
apiConfiguration: ApiConfiguration | undefined
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
|
||||
apiErrorMessage?: string
|
||||
modelIdErrorMessage?: string
|
||||
fromWelcomeView?: boolean
|
||||
errorMessage: string | undefined
|
||||
setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
}
|
||||
|
||||
const ApiOptions = ({
|
||||
uriScheme,
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
apiErrorMessage,
|
||||
modelIdErrorMessage,
|
||||
fromWelcomeView,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
}: ApiOptionsProps) => {
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||
})
|
||||
const [unboundModels, setUnboundModels] = useState<Record<string, ModelInfo>>({
|
||||
[unboundDefaultModelId]: unboundDefaultModelInfo,
|
||||
})
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
})
|
||||
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
|
||||
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
|
||||
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
|
||||
|
|
@ -105,24 +117,100 @@ const ApiOptions = ({
|
|||
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
|
||||
} else if (selectedProvider === "vscode-lm") {
|
||||
vscode.postMessage({ type: "requestVsCodeLmModels" })
|
||||
} else if (selectedProvider === "openai") {
|
||||
vscode.postMessage({
|
||||
type: "refreshOpenAiModels",
|
||||
values: {
|
||||
baseUrl: apiConfiguration?.openAiBaseUrl,
|
||||
apiKey: apiConfiguration?.openAiApiKey,
|
||||
},
|
||||
})
|
||||
} else if (selectedProvider === "openrouter") {
|
||||
vscode.postMessage({ type: "refreshOpenRouterModels", values: {} })
|
||||
} else if (selectedProvider === "glama") {
|
||||
vscode.postMessage({ type: "refreshGlamaModels", values: {} })
|
||||
} else if (selectedProvider === "requesty") {
|
||||
vscode.postMessage({
|
||||
type: "refreshRequestyModels",
|
||||
values: {
|
||||
apiKey: apiConfiguration?.requestyApiKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
250,
|
||||
[selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl],
|
||||
[
|
||||
selectedProvider,
|
||||
apiConfiguration?.ollamaBaseUrl,
|
||||
apiConfiguration?.lmStudioBaseUrl,
|
||||
apiConfiguration?.openAiBaseUrl,
|
||||
apiConfiguration?.openAiApiKey,
|
||||
apiConfiguration?.requestyApiKey,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const apiValidationResult =
|
||||
validateApiConfiguration(apiConfiguration) ||
|
||||
validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
|
||||
setErrorMessage(apiValidationResult)
|
||||
}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
|
||||
const newModels = message.ollamaModels
|
||||
setOllamaModels(newModels)
|
||||
} else if (message.type === "lmStudioModels" && Array.isArray(message.lmStudioModels)) {
|
||||
const newModels = message.lmStudioModels
|
||||
setLmStudioModels(newModels)
|
||||
} else if (message.type === "vsCodeLmModels" && Array.isArray(message.vsCodeLmModels)) {
|
||||
const newModels = message.vsCodeLmModels
|
||||
setVsCodeLmModels(newModels)
|
||||
switch (message.type) {
|
||||
case "ollamaModels":
|
||||
{
|
||||
const newModels = message.ollamaModels ?? []
|
||||
setOllamaModels(newModels)
|
||||
}
|
||||
break
|
||||
case "lmStudioModels":
|
||||
{
|
||||
const newModels = message.lmStudioModels ?? []
|
||||
setLmStudioModels(newModels)
|
||||
}
|
||||
break
|
||||
case "vsCodeLmModels":
|
||||
{
|
||||
const newModels = message.vsCodeLmModels ?? []
|
||||
setVsCodeLmModels(newModels)
|
||||
}
|
||||
break
|
||||
case "glamaModels": {
|
||||
const updatedModels = message.glamaModels ?? {}
|
||||
setGlamaModels({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openRouterModels": {
|
||||
const updatedModels = message.openRouterModels ?? {}
|
||||
setOpenRouterModels({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openAiModels": {
|
||||
const updatedModels = message.openAiModels ?? []
|
||||
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
|
||||
break
|
||||
}
|
||||
case "unboundModels": {
|
||||
const updatedModels = message.unboundModels ?? {}
|
||||
setUnboundModels(updatedModels)
|
||||
break
|
||||
}
|
||||
case "requestyModels": {
|
||||
const updatedModels = message.requestyModels ?? {}
|
||||
setRequestyModels({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
@ -548,6 +636,7 @@ const ApiOptions = ({
|
|||
]}
|
||||
/>
|
||||
</div>
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
|
|
@ -617,7 +706,18 @@ const ApiOptions = ({
|
|||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
<OpenAiModelPicker />
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
modelIdKey="openAiModelId"
|
||||
modelInfoKey="openAiCustomModelInfo"
|
||||
serviceName="OpenAI"
|
||||
serviceUrl="https://platform.openai.com"
|
||||
recommendedModel="gpt-4-turbo-preview"
|
||||
models={openAiModels}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelInfo={openAiModelInfoSaneDefaults}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
|
||||
|
|
@ -705,7 +805,7 @@ const ApiOptions = ({
|
|||
})(),
|
||||
}}
|
||||
title="Maximum number of tokens the model can generate in a single response"
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = parseInt((e.target as HTMLInputElement).value)
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
|
|
@ -752,7 +852,7 @@ const ApiOptions = ({
|
|||
})(),
|
||||
}}
|
||||
title="Total number of tokens (input + output) the model can process in a single request"
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseInt(value)
|
||||
return {
|
||||
|
|
@ -898,7 +998,7 @@ const ApiOptions = ({
|
|||
: "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseFloat(value)
|
||||
return {
|
||||
|
|
@ -943,7 +1043,7 @@ const ApiOptions = ({
|
|||
: "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseFloat(value)
|
||||
return {
|
||||
|
|
@ -980,18 +1080,6 @@ const ApiOptions = ({
|
|||
/>
|
||||
|
||||
{/* end Model Info Configuration */}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||
with Claude models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1012,6 +1100,8 @@ const ApiOptions = ({
|
|||
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
|
||||
{lmStudioModels.length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
|
|
@ -1156,6 +1246,12 @@ const ApiOptions = ({
|
|||
placeholder={"e.g. llama3.1"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
{errorMessage && (
|
||||
<div className="text-vscode-errorForeground text-sm">
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{ollamaModels.length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
|
|
@ -1221,25 +1317,68 @@ const ApiOptions = ({
|
|||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
</p>
|
||||
<UnboundModelPicker />
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
defaultModelId={unboundDefaultModelId}
|
||||
defaultModelInfo={unboundDefaultModelInfo}
|
||||
models={unboundModels}
|
||||
modelInfoKey="unboundModelInfo"
|
||||
modelIdKey="unboundModelId"
|
||||
serviceName="Unbound"
|
||||
serviceUrl="https://api.getunbound.ai/models"
|
||||
recommendedModel={unboundDefaultModelId}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{apiErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
margin: "-10px 0 4px 0",
|
||||
fontSize: 12,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{apiErrorMessage}
|
||||
</p>
|
||||
{selectedProvider === "glama" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration ?? {}}
|
||||
defaultModelId={glamaDefaultModelId}
|
||||
defaultModelInfo={glamaDefaultModelInfo}
|
||||
models={glamaModels}
|
||||
modelInfoKey="glamaModelInfo"
|
||||
modelIdKey="glamaModelId"
|
||||
serviceName="Glama"
|
||||
serviceUrl="https://glama.ai/models"
|
||||
recommendedModel="anthropic/claude-3-7-sonnet"
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "glama" && <GlamaModelPicker />}
|
||||
{selectedProvider === "openrouter" && <OpenRouterModelPicker />}
|
||||
{selectedProvider === "requesty" && <RequestyModelPicker />}
|
||||
{selectedProvider === "openrouter" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={openRouterDefaultModelId}
|
||||
defaultModelInfo={openRouterDefaultModelInfo}
|
||||
models={openRouterModels}
|
||||
modelIdKey="openRouterModelId"
|
||||
modelInfoKey="openRouterModelInfo"
|
||||
serviceName="OpenRouter"
|
||||
serviceUrl="https://openrouter.ai/models"
|
||||
recommendedModel="anthropic/claude-3.7-sonnet"
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
{selectedProvider === "requesty" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={requestyDefaultModelId}
|
||||
defaultModelInfo={requestyDefaultModelInfo}
|
||||
models={requestyModels}
|
||||
modelIdKey="requestyModelId"
|
||||
modelInfoKey="requestyModelInfo"
|
||||
serviceName="Requesty"
|
||||
serviceUrl="https://requesty.ai"
|
||||
recommendedModel="anthropic/claude-3-7-sonnet-latest"
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider !== "glama" &&
|
||||
selectedProvider !== "openrouter" &&
|
||||
|
|
@ -1261,7 +1400,7 @@ const ApiOptions = ({
|
|||
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
|
||||
{selectedProvider === "mistral" && createDropdown(mistralModels)}
|
||||
</div>
|
||||
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
|
|
@ -1299,18 +1438,6 @@ const ApiOptions = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modelIdErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
margin: "-10px 0 4px 0",
|
||||
fontSize: 12,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{modelIdErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { ModelPicker } from "./ModelPicker"
|
||||
import { glamaDefaultModelId } from "../../../../src/shared/api"
|
||||
|
||||
export const GlamaModelPicker = () => (
|
||||
<ModelPicker
|
||||
defaultModelId={glamaDefaultModelId}
|
||||
modelsKey="glamaModels"
|
||||
configKey="glamaModelId"
|
||||
infoKey="glamaModelInfo"
|
||||
refreshMessageType="refreshGlamaModels"
|
||||
serviceName="Glama"
|
||||
serviceUrl="https://glama.ai/models"
|
||||
recommendedModel="anthropic/claude-3-7-sonnet"
|
||||
/>
|
||||
)
|
||||
|
|
@ -1,192 +1,122 @@
|
|||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import debounce from "debounce"
|
||||
import { useMemo, useState, useCallback, useEffect, useRef } from "react"
|
||||
import { useMount } from "react-use"
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
||||
import { useMemo, useState, useCallback, useEffect } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Button,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { normalizeApiConfiguration } from "./ApiOptions"
|
||||
import { ModelInfoView } from "./ModelInfoView"
|
||||
import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
|
||||
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
|
||||
import ApiErrorMessage from "./ApiErrorMessage"
|
||||
|
||||
type ModelProvider = "glama" | "openRouter" | "unbound" | "requesty" | "openAi"
|
||||
type ExtractType<T> = NonNullable<
|
||||
{ [K in keyof ApiConfiguration]: Required<ApiConfiguration>[K] extends T ? K : never }[keyof ApiConfiguration]
|
||||
>
|
||||
|
||||
type ModelKeys<T extends ModelProvider> = `${T}Models`
|
||||
type ConfigKeys<T extends ModelProvider> = `${T}ModelId`
|
||||
type InfoKeys<T extends ModelProvider> = `${T}ModelInfo`
|
||||
type RefreshMessageType<T extends ModelProvider> = `refresh${Capitalize<T>}Models`
|
||||
|
||||
interface ModelPickerProps<T extends ModelProvider = ModelProvider> {
|
||||
defaultModelId: string
|
||||
modelsKey: ModelKeys<T>
|
||||
configKey: ConfigKeys<T>
|
||||
infoKey: InfoKeys<T>
|
||||
refreshMessageType: RefreshMessageType<T>
|
||||
refreshValues?: Record<string, any>
|
||||
type ModelIdKeys = NonNullable<
|
||||
{ [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration]
|
||||
>
|
||||
declare module "react" {
|
||||
interface CSSProperties {
|
||||
// Allow CSS variables
|
||||
[key: `--${string}`]: string | number
|
||||
}
|
||||
}
|
||||
interface ModelPickerProps {
|
||||
defaultModelId?: string
|
||||
models: Record<string, ModelInfo> | null
|
||||
modelIdKey: ModelIdKeys
|
||||
modelInfoKey: ExtractType<ModelInfo>
|
||||
serviceName: string
|
||||
serviceUrl: string
|
||||
recommendedModel: string
|
||||
allowCustomModel?: boolean
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
|
||||
defaultModelInfo?: ModelInfo
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
export const ModelPicker = ({
|
||||
defaultModelId,
|
||||
modelsKey,
|
||||
configKey,
|
||||
infoKey,
|
||||
refreshMessageType,
|
||||
refreshValues,
|
||||
models,
|
||||
modelIdKey,
|
||||
modelInfoKey,
|
||||
serviceName,
|
||||
serviceUrl,
|
||||
recommendedModel,
|
||||
allowCustomModel = false,
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
defaultModelInfo,
|
||||
errorMessage,
|
||||
}: ModelPickerProps) => {
|
||||
const [customModelId, setCustomModelId] = useState("")
|
||||
const [isCustomModel, setIsCustomModel] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [value, setValue] = useState(defaultModelId)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
const prevRefreshValuesRef = useRef<Record<string, any> | undefined>()
|
||||
|
||||
const { apiConfiguration, [modelsKey]: models, onUpdateApiConfig, setApiConfiguration } = useExtensionState()
|
||||
|
||||
const modelIds = useMemo(
|
||||
() => (Array.isArray(models) ? models : Object.keys(models)).sort((a, b) => a.localeCompare(b)),
|
||||
[models],
|
||||
)
|
||||
const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = useMemo(
|
||||
() => normalizeApiConfiguration(apiConfiguration),
|
||||
[apiConfiguration],
|
||||
)
|
||||
|
||||
const onSelectCustomModel = useCallback(
|
||||
(modelId: string) => {
|
||||
setCustomModelId(modelId)
|
||||
const modelInfo = { id: modelId }
|
||||
const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
|
||||
setApiConfiguration(apiConfig)
|
||||
onUpdateApiConfig(apiConfig)
|
||||
setValue(modelId)
|
||||
setOpen(false)
|
||||
setIsCustomModel(false)
|
||||
},
|
||||
[apiConfiguration, configKey, infoKey, onUpdateApiConfig, setApiConfiguration],
|
||||
)
|
||||
|
||||
const onSelect = useCallback(
|
||||
(modelId: string) => {
|
||||
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)
|
||||
setOpen(false)
|
||||
const modelInfo = models?.[modelId]
|
||||
setApiConfigurationField(modelIdKey, modelId)
|
||||
setApiConfigurationField(modelInfoKey, modelInfo ?? defaultModelInfo)
|
||||
},
|
||||
[apiConfiguration, configKey, infoKey, models, onUpdateApiConfig, setApiConfiguration],
|
||||
[modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
|
||||
)
|
||||
|
||||
const debouncedRefreshModels = useMemo(() => {
|
||||
return debounce(() => {
|
||||
const message = refreshValues
|
||||
? { type: refreshMessageType, values: refreshValues }
|
||||
: { type: refreshMessageType }
|
||||
vscode.postMessage(message)
|
||||
}, 100)
|
||||
}, [refreshMessageType, refreshValues])
|
||||
|
||||
useMount(() => {
|
||||
debouncedRefreshModels()
|
||||
return () => debouncedRefreshModels.clear()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!refreshValues) {
|
||||
prevRefreshValuesRef.current = undefined
|
||||
return
|
||||
if (apiConfiguration[modelIdKey] == null && defaultModelId) {
|
||||
onSelect(defaultModelId)
|
||||
}
|
||||
|
||||
// Check if all values in refreshValues are truthy
|
||||
if (Object.values(refreshValues).some((value) => !value)) {
|
||||
prevRefreshValuesRef.current = undefined
|
||||
return
|
||||
}
|
||||
|
||||
// Compare with previous values
|
||||
const prevValues = prevRefreshValuesRef.current
|
||||
if (prevValues && JSON.stringify(prevValues) === JSON.stringify(refreshValues)) {
|
||||
return
|
||||
}
|
||||
|
||||
prevRefreshValuesRef.current = refreshValues
|
||||
debouncedRefreshModels()
|
||||
}, [debouncedRefreshModels, refreshValues])
|
||||
|
||||
useEffect(() => setValue(selectedModelId), [selectedModelId])
|
||||
}, [apiConfiguration, defaultModelId, modelIdKey, onSelect])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="font-semibold">Model</div>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="combobox" role="combobox" aria-expanded={open} className="w-full justify-between">
|
||||
{value ?? "Select model..."}
|
||||
<CaretSortIcon className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search model..." className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modelIds.map((model) => (
|
||||
<CommandItem key={model} value={model} onSelect={onSelect}>
|
||||
{model}
|
||||
<CheckIcon
|
||||
className={cn("ml-auto", value === model ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
{allowCustomModel && (
|
||||
<CommandGroup heading="Custom">
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setIsCustomModel(true)
|
||||
setOpen(false)
|
||||
}}>
|
||||
+ Add custom model
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{selectedModelId && selectedModelInfo && (
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
<Combobox
|
||||
style={errorMessage ? { "--color-vscode-dropdown-border": "var(--color-vscode-errorForeground)" } : {}}
|
||||
type="single"
|
||||
inputValue={apiConfiguration[modelIdKey]}
|
||||
onInputValueChange={onSelect}>
|
||||
<ComboboxInput
|
||||
className="border-vscode-errorForeground tefat"
|
||||
placeholder="Search model..."
|
||||
data-testid="model-input"
|
||||
aria-errormessage={errorMessage}
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No model found.</ComboboxEmpty>
|
||||
{modelIds.map((model) => (
|
||||
<ComboboxItem key={model} value={model}>
|
||||
{model}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
{errorMessage ? (
|
||||
<ApiErrorMessage errorMessage={errorMessage}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||
with Claude models. Less capable models may not work as expected.
|
||||
</span>
|
||||
</p>
|
||||
</ApiErrorMessage>
|
||||
) : (
|
||||
selectedModelId &&
|
||||
selectedModelInfo && (
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<p>
|
||||
The extension automatically fetches the latest list of models available on{" "}
|
||||
|
|
@ -197,28 +127,6 @@ export const ModelPicker = ({
|
|||
<VSCodeLink onClick={() => onSelect(recommendedModel)}>{recommendedModel}.</VSCodeLink>
|
||||
You can also try searching "free" for no-cost options currently available.
|
||||
</p>
|
||||
{allowCustomModel && isCustomModel && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
|
||||
<h3 className="text-lg font-semibold mb-4">Add Custom Model</h3>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full p-2 mb-4 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded"
|
||||
placeholder="Enter model ID"
|
||||
value={customModelId}
|
||||
onChange={(e) => setCustomModelId(e.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setIsCustomModel(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onSelectCustomModel(customModelId)} disabled={!customModelId.trim()}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import React from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { ModelPicker } from "./ModelPicker"
|
||||
|
||||
const OpenAiModelPicker: React.FC = () => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
|
||||
return (
|
||||
<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"
|
||||
allowCustomModel={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenAiModelPicker
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { ModelPicker } from "./ModelPicker"
|
||||
import { openRouterDefaultModelId } from "../../../../src/shared/api"
|
||||
|
||||
export const OpenRouterModelPicker = () => (
|
||||
<ModelPicker
|
||||
defaultModelId={openRouterDefaultModelId}
|
||||
modelsKey="openRouterModels"
|
||||
configKey="openRouterModelId"
|
||||
infoKey="openRouterModelInfo"
|
||||
refreshMessageType="refreshOpenRouterModels"
|
||||
serviceName="OpenRouter"
|
||||
serviceUrl="https://openrouter.ai/models"
|
||||
recommendedModel="anthropic/claude-3.7-sonnet"
|
||||
/>
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { ModelPicker } from "./ModelPicker"
|
||||
import { requestyDefaultModelId } from "../../../../src/shared/api"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
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-7-sonnet-latest"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Dropdown, type DropdownOption } from "vscrui"
|
||||
import { Button, Dropdown, type DropdownOption } from "vscrui"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from "@/components/ui"
|
||||
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
|
||||
import { ApiConfiguration } from "../../../../src/shared/api"
|
||||
|
|
@ -33,19 +32,17 @@ export interface SettingsViewRef {
|
|||
|
||||
const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone }, ref) => {
|
||||
const extensionState = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
|
||||
const [cachedState, setCachedState] = useState(extensionState)
|
||||
const [isChangeDetected, setChangeDetected] = useState(false)
|
||||
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
|
||||
const confirmDialogHandler = useRef<() => void>()
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
// TODO: Reduce WebviewMessage/ExtensionState complexity
|
||||
const { currentApiConfigName } = extensionState
|
||||
const {
|
||||
apiConfiguration,
|
||||
alwaysAllowReadOnly,
|
||||
allowedCommands,
|
||||
alwaysAllowBrowser,
|
||||
|
|
@ -69,6 +66,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
terminalOutputLineLimit,
|
||||
writeDelayMs,
|
||||
} = cachedState
|
||||
|
||||
//Make sure apiConfiguration is initialized and managed by SettingsView
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
||||
useEffect(() => {
|
||||
// Update only when currentApiConfigName is changed
|
||||
|
|
@ -133,20 +133,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isSettingValid = !errorMessage
|
||||
const handleSubmit = () => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
|
||||
const modelIdValidationResult = validateModelId(
|
||||
apiConfiguration,
|
||||
extensionState.glamaModels,
|
||||
extensionState.openRouterModels,
|
||||
)
|
||||
|
||||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
|
||||
if (!apiValidationResult && !modelIdValidationResult) {
|
||||
if (isSettingValid) {
|
||||
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
||||
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
|
||||
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
|
||||
|
|
@ -175,23 +164,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setApiErrorMessage(undefined)
|
||||
setModelIdErrorMessage(undefined)
|
||||
}, [apiConfiguration])
|
||||
|
||||
// Initial validation on mount
|
||||
useEffect(() => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(
|
||||
apiConfiguration,
|
||||
extensionState.glamaModels,
|
||||
extensionState.openRouterModels,
|
||||
)
|
||||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
}, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
|
||||
|
||||
const checkUnsaveChanges = useCallback(
|
||||
(then: () => void) => {
|
||||
if (isChangeDetected) {
|
||||
|
|
@ -285,13 +257,14 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
justifyContent: "space-between",
|
||||
gap: "6px",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
title={isChangeDetected ? "Save changes" : "Nothing changed"}
|
||||
<Button
|
||||
appearance={isSettingValid ? "primary" : "secondary"}
|
||||
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
|
||||
title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
|
||||
onClick={handleSubmit}
|
||||
disabled={!isChangeDetected}>
|
||||
disabled={!isChangeDetected || !isSettingValid}>
|
||||
Save
|
||||
</VSCodeButton>
|
||||
</Button>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
title="Discard unsaved changes and close settings panel"
|
||||
|
|
@ -342,8 +315,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
uriScheme={extensionState.uriScheme}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
apiErrorMessage={apiErrorMessage}
|
||||
modelIdErrorMessage={modelIdErrorMessage}
|
||||
errorMessage={errorMessage}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { ModelPicker } from "./ModelPicker"
|
||||
import { unboundDefaultModelId } from "../../../../src/shared/api"
|
||||
|
||||
export const UnboundModelPicker = () => (
|
||||
<ModelPicker
|
||||
defaultModelId={unboundDefaultModelId}
|
||||
modelsKey="unboundModels"
|
||||
configKey="unboundModelId"
|
||||
infoKey="unboundModelInfo"
|
||||
refreshMessageType="refreshUnboundModels"
|
||||
serviceName="Unbound"
|
||||
serviceUrl="https://api.getunbound.ai/models"
|
||||
recommendedModel={unboundDefaultModelId}
|
||||
/>
|
||||
)
|
||||
|
|
@ -51,6 +51,8 @@ describe("ApiOptions", () => {
|
|||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions
|
||||
errorMessage={undefined}
|
||||
setErrorMessage={() => {}}
|
||||
uriScheme={undefined}
|
||||
apiConfiguration={{}}
|
||||
setApiConfigurationField={() => {}}
|
||||
|
|
@ -69,4 +71,6 @@ describe("ApiOptions", () => {
|
|||
renderApiOptions({ fromWelcomeView: true })
|
||||
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
//TODO: More test cases needed
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { screen, fireEvent, render } from "@testing-library/react"
|
||||
import { act } from "react"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
|
||||
jest.mock("../../../context/ExtensionStateContext", () => ({
|
||||
useExtensionState: jest.fn(),
|
||||
|
|
@ -20,36 +19,40 @@ global.ResizeObserver = MockResizeObserver
|
|||
Element.prototype.scrollIntoView = jest.fn()
|
||||
|
||||
describe("ModelPicker", () => {
|
||||
const mockOnUpdateApiConfig = jest.fn()
|
||||
const mockSetApiConfiguration = jest.fn()
|
||||
|
||||
const mockSetApiConfigurationField = jest.fn()
|
||||
const modelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
const mockModels = {
|
||||
model1: { name: "Model 1", description: "Test model 1", ...modelInfo },
|
||||
model2: { name: "Model 2", description: "Test model 2", ...modelInfo },
|
||||
}
|
||||
const defaultProps = {
|
||||
apiConfiguration: {},
|
||||
defaultModelId: "model1",
|
||||
modelsKey: "glamaModels" as const,
|
||||
configKey: "glamaModelId" as const,
|
||||
infoKey: "glamaModelInfo" as const,
|
||||
refreshMessageType: "refreshGlamaModels" as const,
|
||||
defaultModelInfo: modelInfo,
|
||||
modelIdKey: "glamaModelId" as const,
|
||||
modelInfoKey: "glamaModelInfo" as const,
|
||||
serviceName: "Test Service",
|
||||
serviceUrl: "https://test.service",
|
||||
recommendedModel: "recommended-model",
|
||||
}
|
||||
|
||||
const mockModels = {
|
||||
model1: { name: "Model 1", description: "Test model 1" },
|
||||
model2: { name: "Model 2", description: "Test model 2" },
|
||||
models: mockModels,
|
||||
setApiConfigurationField: mockSetApiConfigurationField,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
apiConfiguration: {},
|
||||
setApiConfiguration: mockSetApiConfiguration,
|
||||
glamaModels: mockModels,
|
||||
onUpdateApiConfig: mockOnUpdateApiConfig,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls onUpdateApiConfig when a model is selected", async () => {
|
||||
it("calls setApiConfigurationField when a model is selected", async () => {
|
||||
await act(async () => {
|
||||
render(<ModelPicker {...defaultProps} />)
|
||||
})
|
||||
|
|
@ -67,20 +70,12 @@ describe("ModelPicker", () => {
|
|||
|
||||
await act(async () => {
|
||||
// Find and click the model item by its value.
|
||||
const modelItem = screen.getByRole("option", { name: "model2" })
|
||||
fireEvent.click(modelItem)
|
||||
const modelItem = screen.getByTestId("model-input")
|
||||
fireEvent.input(modelItem, { target: { value: "model2" } })
|
||||
})
|
||||
|
||||
// Verify the API config was updated.
|
||||
expect(mockSetApiConfiguration).toHaveBeenCalledWith({
|
||||
glamaModelId: "model2",
|
||||
glamaModelInfo: mockModels["model2"],
|
||||
})
|
||||
|
||||
// Verify onUpdateApiConfig was called with the new config.
|
||||
expect(mockOnUpdateApiConfig).toHaveBeenCalledWith({
|
||||
glamaModelId: "model2",
|
||||
glamaModelInfo: mockModels["model2"],
|
||||
})
|
||||
expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelIdKey, "model2")
|
||||
expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelInfoKey, mockModels.model2)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
522
webview-ui/src/components/ui/combobox-primitive.tsx
Normal file
522
webview-ui/src/components/ui/combobox-primitive.tsx
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
/* eslint-disable react/jsx-pascal-case */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { composeEventHandlers } from "@radix-ui/primitive"
|
||||
import { useComposedRefs } from "@radix-ui/react-compose-refs"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import { Primitive } from "@radix-ui/react-primitive"
|
||||
import * as RovingFocusGroupPrimitive from "@radix-ui/react-roving-focus"
|
||||
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
|
||||
export type ComboboxContextProps = {
|
||||
inputValue: string
|
||||
onInputValueChange: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void
|
||||
onInputBlur?: (e: React.FocusEvent<HTMLInputElement, Element>) => void
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
currentTabStopId: string | null
|
||||
onCurrentTabStopIdChange: (currentTabStopId: string | null) => void
|
||||
inputRef: React.RefObject<HTMLInputElement>
|
||||
tagGroupRef: React.RefObject<React.ElementRef<typeof RovingFocusGroupPrimitive.Root>>
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
} & (
|
||||
| Required<Pick<ComboboxSingleProps, "type" | "value" | "onValueChange">>
|
||||
| Required<Pick<ComboboxMultipleProps, "type" | "value" | "onValueChange">>
|
||||
)
|
||||
|
||||
const ComboboxContext = React.createContext<ComboboxContextProps>({
|
||||
type: "single",
|
||||
value: "",
|
||||
onValueChange: () => {},
|
||||
inputValue: "",
|
||||
onInputValueChange: () => {},
|
||||
onInputBlur: () => {},
|
||||
open: false,
|
||||
onOpenChange: () => {},
|
||||
currentTabStopId: null,
|
||||
onCurrentTabStopIdChange: () => {},
|
||||
inputRef: { current: null },
|
||||
tagGroupRef: { current: null },
|
||||
disabled: false,
|
||||
required: false,
|
||||
})
|
||||
|
||||
export const useComboboxContext = () => React.useContext(ComboboxContext)
|
||||
|
||||
export type ComboboxType = "single" | "multiple"
|
||||
|
||||
export interface ComboboxBaseProps
|
||||
extends React.ComponentProps<typeof PopoverPrimitive.Root>,
|
||||
Omit<React.ComponentProps<typeof CommandPrimitive>, "value" | "defaultValue" | "onValueChange"> {
|
||||
type?: ComboboxType | undefined
|
||||
inputValue?: string
|
||||
defaultInputValue?: string
|
||||
onInputValueChange?: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void
|
||||
onInputBlur?: (e: React.FocusEvent<HTMLInputElement, Element>) => void
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export type ComboboxValue<T extends ComboboxType = "single"> = T extends "single"
|
||||
? string
|
||||
: T extends "multiple"
|
||||
? string[]
|
||||
: never
|
||||
|
||||
export interface ComboboxSingleProps {
|
||||
type: "single"
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
onValueChange?: (value: string) => void
|
||||
}
|
||||
|
||||
export interface ComboboxMultipleProps {
|
||||
type: "multiple"
|
||||
value?: string[]
|
||||
defaultValue?: string[]
|
||||
onValueChange?: (value: string[]) => void
|
||||
}
|
||||
|
||||
export type ComboboxProps = ComboboxBaseProps & (ComboboxSingleProps | ComboboxMultipleProps)
|
||||
|
||||
export const Combobox = React.forwardRef(
|
||||
<T extends ComboboxType = "single">(
|
||||
{
|
||||
type = "single" as T,
|
||||
open: openProp,
|
||||
onOpenChange,
|
||||
defaultOpen,
|
||||
modal,
|
||||
children,
|
||||
value: valueProp,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
inputValue: inputValueProp,
|
||||
defaultInputValue,
|
||||
onInputValueChange,
|
||||
onInputBlur,
|
||||
disabled,
|
||||
required,
|
||||
...props
|
||||
}: ComboboxProps,
|
||||
ref: React.ForwardedRef<React.ElementRef<typeof CommandPrimitive>>,
|
||||
) => {
|
||||
const [value = type === "multiple" ? [] : "", setValue] = useControllableState<ComboboxValue<T>>({
|
||||
prop: valueProp as ComboboxValue<T>,
|
||||
defaultProp: defaultValue as ComboboxValue<T>,
|
||||
onChange: onValueChange as (value: ComboboxValue<T>) => void,
|
||||
})
|
||||
const [inputValue = "", setInputValue] = useControllableState({
|
||||
prop: inputValueProp,
|
||||
defaultProp: defaultInputValue,
|
||||
})
|
||||
const [open = false, setOpen] = useControllableState({
|
||||
prop: openProp,
|
||||
defaultProp: defaultOpen,
|
||||
onChange: onOpenChange,
|
||||
})
|
||||
const [currentTabStopId, setCurrentTabStopId] = React.useState<string | null>(null)
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
const tagGroupRef = React.useRef<React.ElementRef<typeof RovingFocusGroupPrimitive.Root>>(null)
|
||||
|
||||
const handleInputValueChange: ComboboxContextProps["onInputValueChange"] = React.useCallback(
|
||||
(inputValue, reason) => {
|
||||
setInputValue(inputValue)
|
||||
onInputValueChange?.(inputValue, reason)
|
||||
},
|
||||
[setInputValue, onInputValueChange],
|
||||
)
|
||||
|
||||
return (
|
||||
<ComboboxContext.Provider
|
||||
value={
|
||||
{
|
||||
type,
|
||||
value,
|
||||
onValueChange: setValue,
|
||||
inputValue,
|
||||
onInputValueChange: handleInputValueChange,
|
||||
onInputBlur,
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
currentTabStopId,
|
||||
onCurrentTabStopIdChange: setCurrentTabStopId,
|
||||
inputRef,
|
||||
tagGroupRef,
|
||||
disabled,
|
||||
required,
|
||||
} as ComboboxContextProps
|
||||
}>
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={setOpen} modal={modal}>
|
||||
<CommandPrimitive ref={ref} {...props}>
|
||||
{children}
|
||||
{!open && <CommandPrimitive.List aria-hidden hidden />}
|
||||
</CommandPrimitive>
|
||||
</PopoverPrimitive.Root>
|
||||
</ComboboxContext.Provider>
|
||||
)
|
||||
},
|
||||
)
|
||||
Combobox.displayName = "Combobox"
|
||||
|
||||
export const ComboboxTagGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RovingFocusGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RovingFocusGroupPrimitive.Root>
|
||||
>((props, ref) => {
|
||||
const { currentTabStopId, onCurrentTabStopIdChange, tagGroupRef, type } = useComboboxContext()
|
||||
|
||||
if (type !== "multiple") {
|
||||
throw new Error('<ComboboxTagGroup> should only be used when type is "multiple"')
|
||||
}
|
||||
|
||||
const composedRefs = useComposedRefs(ref, tagGroupRef)
|
||||
|
||||
return (
|
||||
<RovingFocusGroupPrimitive.Root
|
||||
ref={composedRefs}
|
||||
tabIndex={-1}
|
||||
currentTabStopId={currentTabStopId}
|
||||
onCurrentTabStopIdChange={onCurrentTabStopIdChange}
|
||||
onBlur={() => onCurrentTabStopIdChange(null)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
ComboboxTagGroup.displayName = "ComboboxTagGroup"
|
||||
|
||||
export interface ComboboxTagGroupItemProps
|
||||
extends React.ComponentPropsWithoutRef<typeof RovingFocusGroupPrimitive.Item> {
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ComboboxTagGroupItemContext = React.createContext<Pick<ComboboxTagGroupItemProps, "value" | "disabled">>({
|
||||
value: "",
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const useComboboxTagGroupItemContext = () => React.useContext(ComboboxTagGroupItemContext)
|
||||
|
||||
export const ComboboxTagGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RovingFocusGroupPrimitive.Item>,
|
||||
ComboboxTagGroupItemProps
|
||||
>(({ onClick, onKeyDown, value: valueProp, disabled, ...props }, ref) => {
|
||||
const { value, onValueChange, inputRef, currentTabStopId, type } = useComboboxContext()
|
||||
|
||||
if (type !== "multiple") {
|
||||
throw new Error('<ComboboxTagGroupItem> should only be used when type is "multiple"')
|
||||
}
|
||||
|
||||
const lastItemValue = value.at(-1)
|
||||
|
||||
return (
|
||||
<ComboboxTagGroupItemContext.Provider value={{ value: valueProp, disabled }}>
|
||||
<RovingFocusGroupPrimitive.Item
|
||||
ref={ref}
|
||||
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
|
||||
if (event.key === "Escape") {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
if (event.key === "ArrowRight" && currentTabStopId === lastItemValue) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
if (event.key === "Backspace" || event.key === "Delete") {
|
||||
onValueChange(value.filter((v) => v !== currentTabStopId))
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
})}
|
||||
onClick={composeEventHandlers(onClick, () => disabled && inputRef.current?.focus())}
|
||||
tabStopId={valueProp}
|
||||
focusable={!disabled}
|
||||
data-disabled={disabled}
|
||||
active={valueProp === lastItemValue}
|
||||
{...props}
|
||||
/>
|
||||
</ComboboxTagGroupItemContext.Provider>
|
||||
)
|
||||
})
|
||||
ComboboxTagGroupItem.displayName = "ComboboxTagGroupItem"
|
||||
|
||||
export const ComboboxTagGroupItemRemove = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.button>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.button>
|
||||
>(({ onClick, ...props }, ref) => {
|
||||
const { value, onValueChange, type } = useComboboxContext()
|
||||
|
||||
if (type !== "multiple") {
|
||||
throw new Error('<ComboboxTagGroupItemRemove> should only be used when type is "multiple"')
|
||||
}
|
||||
|
||||
const { value: valueProp, disabled } = useComboboxTagGroupItemContext()
|
||||
|
||||
return (
|
||||
<Primitive.button
|
||||
ref={ref}
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
disabled={disabled}
|
||||
onClick={composeEventHandlers(onClick, () => onValueChange(value.filter((v) => v !== valueProp)))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
ComboboxTagGroupItemRemove.displayName = "ComboboxTagGroupItemRemove"
|
||||
|
||||
export const ComboboxInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
Omit<React.ComponentProps<typeof CommandPrimitive.Input>, "value" | "onValueChange">
|
||||
>(({ onKeyDown, onMouseDown, onFocus, onBlur, ...props }, ref) => {
|
||||
const {
|
||||
type,
|
||||
inputValue,
|
||||
onInputValueChange,
|
||||
onInputBlur,
|
||||
open,
|
||||
onOpenChange,
|
||||
value,
|
||||
onValueChange,
|
||||
inputRef,
|
||||
disabled,
|
||||
required,
|
||||
tagGroupRef,
|
||||
} = useComboboxContext()
|
||||
|
||||
const composedRefs = useComposedRefs(ref, inputRef)
|
||||
|
||||
return (
|
||||
<CommandPrimitive.Input
|
||||
ref={composedRefs}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
value={inputValue}
|
||||
onValueChange={(search) => {
|
||||
if (!open) {
|
||||
onOpenChange(true)
|
||||
}
|
||||
// Schedule input value change to the next tick.
|
||||
setTimeout(() => onInputValueChange(search, "inputChange"))
|
||||
if (!search && type === "single") {
|
||||
onValueChange("")
|
||||
}
|
||||
}}
|
||||
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
if (!open) {
|
||||
event.preventDefault()
|
||||
onOpenChange(true)
|
||||
}
|
||||
}
|
||||
if (type !== "multiple") {
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowLeft" && !inputValue && value.length) {
|
||||
tagGroupRef.current?.focus()
|
||||
}
|
||||
if (event.key === "Backspace" && !inputValue) {
|
||||
onValueChange(value.slice(0, -1))
|
||||
}
|
||||
})}
|
||||
onMouseDown={composeEventHandlers(onMouseDown, () => onOpenChange(!!inputValue || !open))}
|
||||
onFocus={composeEventHandlers(onFocus, () => onOpenChange(true))}
|
||||
onBlur={composeEventHandlers(onBlur, (event) => {
|
||||
if (!event.relatedTarget?.hasAttribute("cmdk-list")) {
|
||||
onInputBlur?.(event)
|
||||
}
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
ComboboxInput.displayName = "ComboboxInput"
|
||||
|
||||
export const ComboboxClear = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.button>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.button>
|
||||
>(({ onClick, ...props }, ref) => {
|
||||
const { value, onValueChange, inputValue, onInputValueChange, type } = useComboboxContext()
|
||||
|
||||
const isValueEmpty = type === "single" ? !value : !value.length
|
||||
|
||||
return (
|
||||
<Primitive.button
|
||||
ref={ref}
|
||||
disabled={isValueEmpty && !inputValue}
|
||||
onClick={composeEventHandlers(onClick, () => {
|
||||
if (type === "single") {
|
||||
onValueChange("")
|
||||
} else {
|
||||
onValueChange([])
|
||||
}
|
||||
onInputValueChange("", "clearClick")
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
ComboboxClear.displayName = "ComboboxClear"
|
||||
|
||||
export const ComboboxTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
export const ComboboxAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
export const ComboboxPortal = PopoverPrimitive.Portal
|
||||
|
||||
export const ComboboxContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ children, onOpenAutoFocus, onInteractOutside, ...props }, ref) => (
|
||||
<PopoverPrimitive.Content
|
||||
asChild
|
||||
ref={ref}
|
||||
onOpenAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())}
|
||||
onCloseAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())}
|
||||
onInteractOutside={composeEventHandlers(onInteractOutside, (event) => {
|
||||
if (event.target instanceof Element && event.target.hasAttribute("cmdk-input")) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})}
|
||||
{...props}>
|
||||
<CommandPrimitive.List>{children}</CommandPrimitive.List>
|
||||
</PopoverPrimitive.Content>
|
||||
))
|
||||
ComboboxContent.displayName = "ComboboxContent"
|
||||
|
||||
export const ComboboxEmpty = CommandPrimitive.Empty
|
||||
|
||||
export const ComboboxLoading = CommandPrimitive.Loading
|
||||
|
||||
export interface ComboboxItemProps extends Omit<React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>, "value"> {
|
||||
value: string
|
||||
}
|
||||
|
||||
const ComboboxItemContext = React.createContext({ isSelected: false })
|
||||
|
||||
const useComboboxItemContext = () => React.useContext(ComboboxItemContext)
|
||||
|
||||
const findComboboxItemText = (children: React.ReactNode) => {
|
||||
let text = ""
|
||||
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (text) {
|
||||
return
|
||||
}
|
||||
|
||||
if (React.isValidElement<{ children: React.ReactNode }>(child)) {
|
||||
if (child.type === ComboboxItemText) {
|
||||
text = child.props.children as string
|
||||
} else {
|
||||
text = findComboboxItemText(child.props.children)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
export const ComboboxItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, ComboboxItemProps>(
|
||||
({ value: valueProp, children, onMouseDown, ...props }, ref) => {
|
||||
const { type, value, onValueChange, onInputValueChange, onOpenChange } = useComboboxContext()
|
||||
|
||||
const inputValue = React.useMemo(() => findComboboxItemText(children), [children])
|
||||
|
||||
const isSelected = type === "single" ? value === valueProp : value.includes(valueProp)
|
||||
|
||||
return (
|
||||
<ComboboxItemContext.Provider value={{ isSelected }}>
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
onMouseDown={composeEventHandlers(onMouseDown, (event) => event.preventDefault())}
|
||||
onSelect={() => {
|
||||
if (type === "multiple") {
|
||||
onValueChange(
|
||||
value.includes(valueProp)
|
||||
? value.filter((v) => v !== valueProp)
|
||||
: [...value, valueProp],
|
||||
)
|
||||
onInputValueChange("", "itemSelect")
|
||||
} else {
|
||||
onValueChange(valueProp)
|
||||
onInputValueChange(inputValue, "itemSelect")
|
||||
// Schedule open change to the next tick.
|
||||
setTimeout(() => onOpenChange(false))
|
||||
}
|
||||
}}
|
||||
value={inputValue}
|
||||
{...props}>
|
||||
{children}
|
||||
</CommandPrimitive.Item>
|
||||
</ComboboxItemContext.Provider>
|
||||
)
|
||||
},
|
||||
)
|
||||
ComboboxItem.displayName = "ComboboxItem"
|
||||
|
||||
export const ComboboxItemIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.span>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.span>
|
||||
>((props, ref) => {
|
||||
const { isSelected } = useComboboxItemContext()
|
||||
|
||||
if (!isSelected) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Primitive.span ref={ref} aria-hidden {...props} />
|
||||
})
|
||||
ComboboxItemIndicator.displayName = "ComboboxItemIndicator"
|
||||
|
||||
export interface ComboboxItemTextProps extends React.ComponentPropsWithoutRef<typeof React.Fragment> {
|
||||
children: string
|
||||
}
|
||||
|
||||
export const ComboboxItemText = (props: ComboboxItemTextProps) => <React.Fragment {...props} />
|
||||
ComboboxItemText.displayName = "ComboboxItemText"
|
||||
|
||||
export const ComboboxGroup = CommandPrimitive.Group
|
||||
|
||||
export const ComboboxSeparator = CommandPrimitive.Separator
|
||||
|
||||
const Root = Combobox
|
||||
const TagGroup = ComboboxTagGroup
|
||||
const TagGroupItem = ComboboxTagGroupItem
|
||||
const TagGroupItemRemove = ComboboxTagGroupItemRemove
|
||||
const Input = ComboboxInput
|
||||
const Clear = ComboboxClear
|
||||
const Trigger = ComboboxTrigger
|
||||
const Anchor = ComboboxAnchor
|
||||
const Portal = ComboboxPortal
|
||||
const Content = ComboboxContent
|
||||
const Empty = ComboboxEmpty
|
||||
const Loading = ComboboxLoading
|
||||
const Item = ComboboxItem
|
||||
const ItemIndicator = ComboboxItemIndicator
|
||||
const ItemText = ComboboxItemText
|
||||
const Group = ComboboxGroup
|
||||
const Separator = ComboboxSeparator
|
||||
|
||||
export {
|
||||
Root,
|
||||
TagGroup,
|
||||
TagGroupItem,
|
||||
TagGroupItemRemove,
|
||||
Input,
|
||||
Clear,
|
||||
Trigger,
|
||||
Anchor,
|
||||
Portal,
|
||||
Content,
|
||||
Empty,
|
||||
Loading,
|
||||
Item,
|
||||
ItemIndicator,
|
||||
ItemText,
|
||||
Group,
|
||||
Separator,
|
||||
}
|
||||
177
webview-ui/src/components/ui/combobox.tsx
Normal file
177
webview-ui/src/components/ui/combobox.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Slottable } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { Check, ChevronsUpDown, Loader, X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as ComboboxPrimitive from "@/components/ui/combobox-primitive"
|
||||
import { badgeVariants } from "@/components/ui/badge"
|
||||
// import * as ComboboxPrimitive from "@/registry/default/ui/combobox-primitive"
|
||||
import {
|
||||
InputBase,
|
||||
InputBaseAdornmentButton,
|
||||
InputBaseControl,
|
||||
InputBaseFlexWrapper,
|
||||
InputBaseInput,
|
||||
} from "@/components/ui/input-base"
|
||||
|
||||
export const Combobox = ComboboxPrimitive.Root
|
||||
|
||||
const ComboboxInputBase = React.forwardRef<
|
||||
React.ElementRef<typeof InputBase>,
|
||||
React.ComponentPropsWithoutRef<typeof InputBase>
|
||||
>(({ children, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Anchor asChild>
|
||||
<InputBase ref={ref} {...props}>
|
||||
{children}
|
||||
<ComboboxPrimitive.Clear asChild>
|
||||
<InputBaseAdornmentButton>
|
||||
<X />
|
||||
</InputBaseAdornmentButton>
|
||||
</ComboboxPrimitive.Clear>
|
||||
<ComboboxPrimitive.Trigger asChild>
|
||||
<InputBaseAdornmentButton>
|
||||
<ChevronsUpDown />
|
||||
</InputBaseAdornmentButton>
|
||||
</ComboboxPrimitive.Trigger>
|
||||
</InputBase>
|
||||
</ComboboxPrimitive.Anchor>
|
||||
))
|
||||
ComboboxInputBase.displayName = "ComboboxInputBase"
|
||||
|
||||
export const ComboboxInput = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Input>
|
||||
>((props, ref) => (
|
||||
<ComboboxInputBase>
|
||||
<InputBaseControl>
|
||||
<ComboboxPrimitive.Input asChild>
|
||||
<InputBaseInput ref={ref} {...props} />
|
||||
</ComboboxPrimitive.Input>
|
||||
</InputBaseControl>
|
||||
</ComboboxInputBase>
|
||||
))
|
||||
ComboboxInput.displayName = "ComboboxInput"
|
||||
|
||||
export const ComboboxTagsInput = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Input>
|
||||
>(({ children, ...props }, ref) => (
|
||||
<ComboboxInputBase>
|
||||
<ComboboxPrimitive.ComboboxTagGroup asChild>
|
||||
<InputBaseFlexWrapper className="flex items-center gap-2">
|
||||
{children}
|
||||
<InputBaseControl>
|
||||
<ComboboxPrimitive.Input asChild>
|
||||
<InputBaseInput ref={ref} {...props} />
|
||||
</ComboboxPrimitive.Input>
|
||||
</InputBaseControl>
|
||||
</InputBaseFlexWrapper>
|
||||
</ComboboxPrimitive.ComboboxTagGroup>
|
||||
</ComboboxInputBase>
|
||||
))
|
||||
ComboboxTagsInput.displayName = "ComboboxTagsInput"
|
||||
|
||||
export const ComboboxTag = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.ComboboxTagGroupItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.ComboboxTagGroupItem>
|
||||
>(({ children, className, ...props }, ref) => (
|
||||
<ComboboxPrimitive.ComboboxTagGroupItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
badgeVariants({ variant: "outline" }),
|
||||
"group gap-1 pr-1.5 data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
<Slottable>{children}</Slottable>
|
||||
<ComboboxPrimitive.ComboboxTagGroupItemRemove className="group-data-[disabled]:pointer-events-none">
|
||||
<X className="size-4" />
|
||||
<span className="sr-only">Remove</span>
|
||||
</ComboboxPrimitive.ComboboxTagGroupItemRemove>
|
||||
</ComboboxPrimitive.ComboboxTagGroupItem>
|
||||
))
|
||||
ComboboxTag.displayName = "ComboboxTag"
|
||||
|
||||
export const ComboboxContent = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Content>
|
||||
>(({ className, align = "start", alignOffset = 0, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Portal>
|
||||
<ComboboxPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
className={cn(
|
||||
"min-w-72 border-vscode-dropdown-border relative z-50 left-0 max-h-96 w-[--radix-popover-trigger-width] overflow-y-auto overflow-x-hidden rounded-xs border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ComboboxPrimitive.Portal>
|
||||
))
|
||||
ComboboxContent.displayName = "ComboboxContent"
|
||||
|
||||
export const ComboboxEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Empty>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Empty ref={ref} className={cn("py-6 text-center text-sm", className)} {...props} />
|
||||
))
|
||||
ComboboxEmpty.displayName = "ComboboxEmpty"
|
||||
|
||||
export const ComboboxLoading = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Loading>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Loading>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Loading
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center px-1.5 py-2", className)}
|
||||
{...props}>
|
||||
<Loader className="size-4 animate-spin [mask:conic-gradient(transparent_45deg,_white)]" />
|
||||
</ComboboxPrimitive.Loading>
|
||||
))
|
||||
ComboboxLoading.displayName = "ComboboxLoading"
|
||||
|
||||
export const ComboboxGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-semibold",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ComboboxGroup.displayName = "ComboboxGroup"
|
||||
|
||||
const ComboboxSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
|
||||
))
|
||||
ComboboxSeparator.displayName = "ComboboxSeparator"
|
||||
|
||||
export const comboboxItemStyle = cva(
|
||||
"relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-vscode-dropdown-foreground data-[disabled=true]:opacity-50",
|
||||
)
|
||||
|
||||
export const ComboboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ComboboxPrimitive.Item>,
|
||||
Omit<React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Item>, "children"> &
|
||||
Pick<React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.ItemText>, "children">
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ComboboxPrimitive.Item ref={ref} className={cn(comboboxItemStyle(), className)} {...props}>
|
||||
<ComboboxPrimitive.ItemText>{children}</ComboboxPrimitive.ItemText>
|
||||
<ComboboxPrimitive.ItemIndicator className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<Check className="size-4" />
|
||||
</ComboboxPrimitive.ItemIndicator>
|
||||
</ComboboxPrimitive.Item>
|
||||
))
|
||||
ComboboxItem.displayName = "ComboboxItem"
|
||||
157
webview-ui/src/components/ui/input-base.tsx
Normal file
157
webview-ui/src/components/ui/input-base.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* eslint-disable react/jsx-no-comment-textnodes */
|
||||
/* eslint-disable react/jsx-pascal-case */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { composeEventHandlers } from "@radix-ui/primitive"
|
||||
import { composeRefs } from "@radix-ui/react-compose-refs"
|
||||
import { Primitive } from "@radix-ui/react-primitive"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "./button"
|
||||
|
||||
export type InputBaseContextProps = Pick<InputBaseProps, "autoFocus" | "disabled"> & {
|
||||
controlRef: React.RefObject<HTMLElement>
|
||||
onFocusedChange: (focused: boolean) => void
|
||||
}
|
||||
|
||||
const InputBaseContext = React.createContext<InputBaseContextProps>({
|
||||
autoFocus: false,
|
||||
controlRef: { current: null },
|
||||
disabled: false,
|
||||
onFocusedChange: () => {},
|
||||
})
|
||||
|
||||
const useInputBaseContext = () => React.useContext(InputBaseContext)
|
||||
|
||||
export interface InputBaseProps extends React.ComponentPropsWithoutRef<typeof Primitive.div> {
|
||||
autoFocus?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const InputBase = React.forwardRef<React.ElementRef<typeof Primitive.div>, InputBaseProps>(
|
||||
({ autoFocus, disabled, className, onClick, ...props }, ref) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [focused, setFocused] = React.useState(false)
|
||||
|
||||
const controlRef = React.useRef<HTMLElement>(null)
|
||||
|
||||
return (
|
||||
<InputBaseContext.Provider
|
||||
value={{
|
||||
autoFocus,
|
||||
controlRef,
|
||||
disabled,
|
||||
onFocusedChange: setFocused,
|
||||
}}>
|
||||
<Primitive.div
|
||||
ref={ref}
|
||||
onClick={composeEventHandlers(onClick, (event) => {
|
||||
// Based on MUI's <InputBase /> implementation.
|
||||
// https://github.com/mui/material-ui/blob/master/packages/mui-material/src/InputBase/InputBase.js#L458~L460
|
||||
if (controlRef.current && event.currentTarget === event.target) {
|
||||
controlRef.current.focus()
|
||||
}
|
||||
})}
|
||||
className={cn(
|
||||
"flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-0.5 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</InputBaseContext.Provider>
|
||||
)
|
||||
},
|
||||
)
|
||||
InputBase.displayName = "InputBase"
|
||||
|
||||
export const InputBaseFlexWrapper = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.div>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.div>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitive.div ref={ref} className={cn("flex flex-1 flex-wrap", className)} {...props} />
|
||||
))
|
||||
InputBaseFlexWrapper.displayName = "InputBaseFlexWrapper"
|
||||
|
||||
export const InputBaseControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ onFocus, onBlur, ...props }, ref) => {
|
||||
const { controlRef, autoFocus, disabled, onFocusedChange } = useInputBaseContext()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={composeRefs(controlRef, ref)}
|
||||
autoFocus={autoFocus}
|
||||
onFocus={composeEventHandlers(onFocus, () => onFocusedChange(true))}
|
||||
onBlur={composeEventHandlers(onBlur, () => onFocusedChange(false))}
|
||||
{...{ disabled }}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
InputBaseControl.displayName = "InputBaseControl"
|
||||
|
||||
export interface InputBaseAdornmentProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||
asChild?: boolean
|
||||
disablePointerEvents?: boolean
|
||||
}
|
||||
|
||||
export const InputBaseAdornment = React.forwardRef<React.ElementRef<"div">, InputBaseAdornmentProps>(
|
||||
({ className, disablePointerEvents, asChild, children, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : typeof children === "string" ? "p" : "div"
|
||||
|
||||
const isAction = React.isValidElement(children) && children.type === InputBaseAdornmentButton
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center text-muted-foreground [&_svg]:size-4",
|
||||
(!isAction || disablePointerEvents) && "pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
},
|
||||
)
|
||||
InputBaseAdornment.displayName = "InputBaseAdornment"
|
||||
|
||||
export const InputBaseAdornmentButton = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentPropsWithoutRef<typeof Button>
|
||||
>(({ type = "button", variant = "ghost", size = "icon", disabled: disabledProp, className, ...props }, ref) => {
|
||||
const { disabled } = useInputBaseContext()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
type={type}
|
||||
variant={variant}
|
||||
size={size}
|
||||
disabled={disabled || disabledProp}
|
||||
className={cn("size-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
InputBaseAdornmentButton.displayName = "InputBaseAdornmentButton"
|
||||
|
||||
export const InputBaseInput = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.input>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitive.input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-full flex-1 bg-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-none disabled:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputBaseInput.displayName = "InputBaseInput"
|
||||
|
|
@ -42,6 +42,8 @@ const WelcomeView = () => {
|
|||
apiConfiguration={apiConfiguration || {}}
|
||||
uriScheme={uriScheme}
|
||||
setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })}
|
||||
errorMessage={errorMessage}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,7 @@
|
|||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
|
||||
import {
|
||||
ApiConfiguration,
|
||||
ModelInfo,
|
||||
glamaDefaultModelId,
|
||||
glamaDefaultModelInfo,
|
||||
openRouterDefaultModelId,
|
||||
openRouterDefaultModelInfo,
|
||||
unboundDefaultModelId,
|
||||
unboundDefaultModelInfo,
|
||||
requestyDefaultModelId,
|
||||
requestyDefaultModelInfo,
|
||||
} from "../../../src/shared/api"
|
||||
import { ApiConfiguration } from "../../../src/shared/api"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { findLastIndex } from "../../../src/shared/array"
|
||||
|
|
@ -26,11 +15,6 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
didHydrateState: boolean
|
||||
showWelcome: boolean
|
||||
theme: any
|
||||
glamaModels: Record<string, ModelInfo>
|
||||
requestyModels: Record<string, ModelInfo>
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
unboundModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
mcpServers: McpServer[]
|
||||
currentCheckpoint?: string
|
||||
filePaths: string[]
|
||||
|
|
@ -70,7 +54,6 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setRateLimitSeconds: (value: number) => void
|
||||
setCurrentApiConfigName: (value: string) => void
|
||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||
mode: Mode
|
||||
setMode: (value: Mode) => void
|
||||
setCustomModePrompts: (value: CustomModePrompts) => void
|
||||
|
|
@ -125,21 +108,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
const [theme, setTheme] = useState<any>(undefined)
|
||||
const [filePaths, setFilePaths] = useState<string[]>([])
|
||||
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||
})
|
||||
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
const [unboundModels, setUnboundModels] = useState<Record<string, ModelInfo>>({
|
||||
[unboundDefaultModelId]: unboundDefaultModelInfo,
|
||||
})
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
})
|
||||
|
||||
const [openAiModels, setOpenAiModels] = useState<string[]>([])
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
|
||||
|
||||
|
|
@ -147,18 +117,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
[],
|
||||
)
|
||||
|
||||
const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
|
||||
setState((currentState) => {
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentState.currentApiConfigName,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, ...apiConfig },
|
||||
})
|
||||
return currentState // No state update needed
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
|
@ -203,40 +161,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
})
|
||||
break
|
||||
}
|
||||
case "glamaModels": {
|
||||
const updatedModels = message.glamaModels ?? {}
|
||||
setGlamaModels({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openRouterModels": {
|
||||
const updatedModels = message.openRouterModels ?? {}
|
||||
setOpenRouterModels({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openAiModels": {
|
||||
const updatedModels = message.openAiModels ?? []
|
||||
setOpenAiModels(updatedModels)
|
||||
break
|
||||
}
|
||||
case "unboundModels": {
|
||||
const updatedModels = message.unboundModels ?? {}
|
||||
setUnboundModels(updatedModels)
|
||||
break
|
||||
}
|
||||
case "requestyModels": {
|
||||
const updatedModels = message.requestyModels ?? {}
|
||||
setRequestyModels({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "mcpServers": {
|
||||
setMcpServers(message.mcpServers ?? [])
|
||||
break
|
||||
|
|
@ -265,11 +189,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
didHydrateState,
|
||||
showWelcome,
|
||||
theme,
|
||||
glamaModels,
|
||||
requestyModels,
|
||||
openRouterModels,
|
||||
openAiModels,
|
||||
unboundModels,
|
||||
mcpServers,
|
||||
currentCheckpoint,
|
||||
filePaths,
|
||||
|
|
@ -317,7 +236,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
|
||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||
setListApiConfigMeta,
|
||||
onUpdateApiConfig,
|
||||
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
||||
setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
|
||||
setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
ApiConfiguration,
|
||||
glamaDefaultModelId,
|
||||
openRouterDefaultModelId,
|
||||
unboundDefaultModelId,
|
||||
} from "../../../src/shared/api"
|
||||
import { ApiConfiguration } from "../../../src/shared/api"
|
||||
import { ModelInfo } from "../../../src/shared/api"
|
||||
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
||||
if (apiConfiguration) {
|
||||
|
|
@ -86,7 +81,7 @@ export function validateModelId(
|
|||
if (apiConfiguration) {
|
||||
switch (apiConfiguration.apiProvider) {
|
||||
case "glama":
|
||||
const glamaModelId = apiConfiguration.glamaModelId || glamaDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||
const glamaModelId = apiConfiguration.glamaModelId
|
||||
if (!glamaModelId) {
|
||||
return "You must provide a model ID."
|
||||
}
|
||||
|
|
@ -96,7 +91,7 @@ export function validateModelId(
|
|||
}
|
||||
break
|
||||
case "openrouter":
|
||||
const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
|
||||
const modelId = apiConfiguration.openRouterModelId
|
||||
if (!modelId) {
|
||||
return "You must provide a model ID."
|
||||
}
|
||||
|
|
@ -106,7 +101,7 @@ export function validateModelId(
|
|||
}
|
||||
break
|
||||
case "unbound":
|
||||
const unboundModelId = apiConfiguration.unboundModelId || unboundDefaultModelId
|
||||
const unboundModelId = apiConfiguration.unboundModelId
|
||||
if (!unboundModelId) {
|
||||
return "You must provide a model ID."
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue