Add option to choose different models

This commit is contained in:
Saoud Rizwan
2024-08-11 00:28:22 -04:00
parent a863b26b7a
commit f54774b943
22 changed files with 487 additions and 151 deletions

View File

@@ -1,34 +1,78 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import React, { useMemo } from "react"
import {
ApiConfiguration,
ApiModelId,
ModelInfo,
anthropicDefaultModelId,
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
openRouterDefaultModelId,
openRouterModels,
} from "../../../src/shared/api"
interface ApiOptionsProps {
showModelOptions: boolean
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
}
const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfiguration }) => {
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfiguration, setApiConfiguration }) => {
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
}
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
/*
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
const createDropdown = (models: Record<string, ModelInfo>) => {
return (
<VSCodeDropdown
id="model-id"
value={selectedModelId}
onChange={handleInputChange("apiModelId")}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(models).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
)
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<div className="dropdown-container">
<label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
<VSCodeDropdown
id="api-provider"
value={apiConfiguration?.apiProvider || "anthropic"}
onChange={handleInputChange("apiProvider")}>
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
</VSCodeDropdown>
</div>
{apiConfiguration?.apiProvider === "anthropic" && (
{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
value={apiConfiguration?.apiKey || ""}
@@ -51,7 +95,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
</div>
)}
{apiConfiguration?.apiProvider === "openrouter" && (
{selectedProvider === "openrouter" && (
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
@@ -74,7 +118,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
</div>
)}
{apiConfiguration?.apiProvider === "bedrock" && (
{selectedProvider === "bedrock" && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
@@ -100,9 +144,9 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* Currently Claude 3.5 Sonnet is only available in us-east-1 */}
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
<VSCodeOption value="us-east-1">US East (N. Virginia)</VSCodeOption>
{/* <VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption>
<VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption>
<VSCodeOption value="us-west-1">US West (N. California)</VSCodeOption>
<VSCodeOption value="us-west-2">US West (Oregon)</VSCodeOption>
<VSCodeOption value="af-south-1">Africa (Cape Town)</VSCodeOption>
@@ -120,7 +164,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
<VSCodeOption value="eu-west-3">Europe (Paris)</VSCodeOption>
<VSCodeOption value="eu-north-1">Europe (Stockholm)</VSCodeOption>
<VSCodeOption value="me-south-1">Middle East (Bahrain)</VSCodeOption>
<VSCodeOption value="sa-east-1">South America (São Paulo)</VSCodeOption> */}
<VSCodeOption value="sa-east-1">South America (São Paulo)</VSCodeOption>
</VSCodeDropdown>
</div>
<p
@@ -138,8 +182,91 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
</p>
</div>
)}
{showModelOptions && (
<>
<div className="dropdown-container">
<label htmlFor="model-id">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
</div>
<ModelInfoView modelInfo={selectedModelInfo} />
</>
)}
</div>
)
}
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
return (
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
<span
style={{
fontWeight: 500,
color: modelInfo.supportsImages
? "var(--vscode-testing-iconPassed)"
: "var(--vscode-errorForeground)",
}}>
<i
className={`codicon codicon-${modelInfo.supportsImages ? "check" : "x"}`}
style={{
marginRight: 4,
marginBottom: modelInfo.supportsImages ? 1 : -1,
fontSize: modelInfo.supportsImages ? 11 : 13,
fontWeight: 700,
display: "inline-block",
verticalAlign: "bottom",
}}></i>
{modelInfo.supportsImages ? "Supports images" : "Does not support images"}
</span>
<br />
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens.toLocaleString()} tokens
<br />
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)} per million tokens
<br />
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)} per million
tokens
</p>
)
}
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const provider = apiConfiguration?.apiProvider || "anthropic"
const modelId = apiConfiguration?.apiModelId
const getProviderData = (models: Record<string, ModelInfo>, defaultId: ApiModelId) => {
let selectedModelId: ApiModelId
let selectedModelInfo: ModelInfo
if (modelId && modelId in models) {
selectedModelId = modelId
selectedModelInfo = models[modelId]
} else {
selectedModelId = defaultId
selectedModelInfo = models[defaultId]
}
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "openrouter":
return getProviderData(openRouterModels, openRouterDefaultModelId)
case "bedrock":
return getProviderData(bedrockModels, bedrockDefaultModelId)
}
}
export default ApiOptions

View File

@@ -1,11 +1,11 @@
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage"
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import Markdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING } from "../utils/combineCommandSequences"
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
import CodeBlock from "./CodeBlock/CodeBlock"
import Markdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import Thumbnails from "./Thumbnails"
interface ChatRowProps {

View File

@@ -1,18 +1,18 @@
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"
import DynamicTextArea from "react-textarea-autosize"
import { useEvent, useMount } from "react-use"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
import { combineApiRequests } from "../utils/combineApiRequests"
import { combineCommandSequences } from "../utils/combineCommandSequences"
import { getApiMetrics } from "../utils/getApiMetrics"
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
import { vscode } from "../utils/vscode"
import Announcement from "./Announcement"
import ChatRow from "./ChatRow"
import TaskHeader from "./TaskHeader"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import Announcement from "./Announcement"
import Thumbnails from "./Thumbnails"
interface ChatViewProps {
@@ -21,6 +21,7 @@ interface ChatViewProps {
isHidden: boolean
vscodeThemeName?: string
showAnnouncement: boolean
selectedModelSupportsImages: boolean
hideAnnouncement: () => void
}
@@ -32,6 +33,7 @@ const ChatView = ({
isHidden,
vscodeThemeName,
showAnnouncement,
selectedModelSupportsImages,
hideAnnouncement,
}: ChatViewProps) => {
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
@@ -278,6 +280,11 @@ const ChatView = ({
}
const handlePaste = async (e: React.ClipboardEvent) => {
if (shouldDisableImages) {
e.preventDefault()
return
}
const items = e.clipboardData.items
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
const imageItems = Array.from(items).filter((item) => {
@@ -412,6 +419,12 @@ const ChatView = ({
return [text, false]
}, [task, messages])
const shouldDisableImages =
!selectedModelSupportsImages ||
textAreaDisabled ||
selectedImages.length >= MAX_IMAGES_PER_MESSAGE ||
isInputPipingToStdin
return (
<div
style={{
@@ -590,9 +603,7 @@ const ChatView = ({
height: "calc(100% - 20px)", // Full height minus top and bottom padding
}}>
<VSCodeButton
disabled={
textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE || isInputPipingToStdin
}
disabled={shouldDisableImages}
appearance="icon"
aria-label="Attach Images"
onClick={selectImages}

View File

@@ -1,6 +1,6 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { ApiConfiguration } from "../../../src/shared/api"
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
@@ -60,78 +60,92 @@ const SettingsView = ({
*/
return (
<div style={{ margin: "0 auto", paddingTop: "10px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div style={{ marginBottom: 5 }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
</div>
<div style={{ marginBottom: "20px" }}>
<VSCodeTextField
value={maxRequestsPerTask}
style={{ width: "100%" }}
placeholder="20"
onInput={(e: any) => setMaxRequestsPerTask(e.target?.value)}>
<span style={{ fontWeight: "500" }}>Maximum # Requests Per Task</span>
</VSCodeTextField>
<p
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: "10px 18px 18px 20px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<div style={{ flexGrow: 1, overflow: "auto" }}>
<div
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
}}>
If Claude Dev reaches this limit, it will pause and ask for your permission before making additional
requests.
</p>
{maxRequestsErrorMessage && (
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div style={{ marginBottom: 5 }}>
<ApiOptions
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
showModelOptions={true}
/>
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
</div>
<div style={{ marginBottom: "20px" }}>
<VSCodeTextField
value={maxRequestsPerTask}
style={{ width: "100%" }}
placeholder="20"
onInput={(e: any) => setMaxRequestsPerTask(e.target?.value)}>
<span style={{ fontWeight: "500" }}>Maximum # Requests Per Task</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
color: "var(--vscode-descriptionForeground)",
}}>
{maxRequestsErrorMessage}
If Claude Dev reaches this limit, it will pause and ask for your permission before making
additional requests.
</p>
)}
{maxRequestsErrorMessage && (
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
}}>
{maxRequestsErrorMessage}
</p>
)}
</div>
</div>
<VSCodeDivider />
<div
style={{
marginTop: "20px",
textAlign: "center",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
lineHeight: "1.2",
}}>
<p style={{ wordWrap: "break-word" }}>
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev" style={{ display: "inline" }}>
https://github.com/saoudrizwan/claude-dev
</VSCodeLink>
</p>
<p style={{ fontStyle: "italic" }}>v{version}</p>
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
</div>
</div>
)

View File

@@ -2,8 +2,8 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useRef, useState } from "react"
import TextTruncate from "react-text-truncate"
import { useWindowSize } from "react-use"
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
import { vscode } from "../utils/vscode"
import { ClaudeMessage } from "@shared/ExtensionMessage"
import Thumbnails from "./Thumbnails"
interface TaskHeaderProps {

View File

@@ -1,4 +1,4 @@
import { ApiConfiguration } from "@shared/api"
import { ApiConfiguration } from "../../../src/shared/api"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { validateApiConfiguration } from "../utils/validate"
@@ -40,7 +40,11 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiConfiguration, setApiConfi
<b>To get started, this extension needs an API key for Claude 3.5 Sonnet:</b>
<div style={{ marginTop: "15px" }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<ApiOptions
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
showModelOptions={false}
/>
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>