@@ -2,13 +2,67 @@ import React, { useState, useEffect, useCallback } from "react";
22import { ChevronDown , ChevronRight , Check , X } from "lucide-react" ;
33import type { ProvidersConfigMap } from "../types" ;
44import { SUPPORTED_PROVIDERS , PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers" ;
5+ import type { ProviderName } from "@/common/constants/providers" ;
6+
7+ interface FieldConfig {
8+ key : string ;
9+ label : string ;
10+ placeholder : string ;
11+ type : "secret" | "text" ;
12+ optional ?: boolean ;
13+ }
14+
15+ /**
16+ * Get provider-specific field configuration.
17+ * Most providers use API Key + Base URL, but some (like Bedrock) have different needs.
18+ */
19+ function getProviderFields ( provider : ProviderName ) : FieldConfig [ ] {
20+ if ( provider === "bedrock" ) {
21+ return [
22+ { key : "region" , label : "Region" , placeholder : "us-east-1" , type : "text" } ,
23+ {
24+ key : "bearerToken" ,
25+ label : "Bearer Token" ,
26+ placeholder : "AWS_BEARER_TOKEN_BEDROCK" ,
27+ type : "secret" ,
28+ optional : true ,
29+ } ,
30+ {
31+ key : "accessKeyId" ,
32+ label : "Access Key ID" ,
33+ placeholder : "AWS Access Key ID" ,
34+ type : "secret" ,
35+ optional : true ,
36+ } ,
37+ {
38+ key : "secretAccessKey" ,
39+ label : "Secret Access Key" ,
40+ placeholder : "AWS Secret Access Key" ,
41+ type : "secret" ,
42+ optional : true ,
43+ } ,
44+ ] ;
45+ }
46+
47+ // Default for most providers
48+ return [
49+ { key : "apiKey" , label : "API Key" , placeholder : "Enter API key" , type : "secret" } ,
50+ {
51+ key : "baseUrl" ,
52+ label : "Base URL" ,
53+ placeholder : "https://api.example.com" ,
54+ type : "text" ,
55+ optional : true ,
56+ } ,
57+ ] ;
58+ }
559
660export function ProvidersSection ( ) {
761 const [ config , setConfig ] = useState < ProvidersConfigMap > ( { } ) ;
862 const [ expandedProvider , setExpandedProvider ] = useState < string | null > ( null ) ;
963 const [ editingField , setEditingField ] = useState < {
1064 provider : string ;
11- field : "apiKey" | "baseUrl" ;
65+ field : string ;
1266 } | null > ( null ) ;
1367 const [ editValue , setEditValue ] = useState ( "" ) ;
1468 const [ saving , setSaving ] = useState ( false ) ;
@@ -26,11 +80,14 @@ export function ProvidersSection() {
2680 setEditingField ( null ) ;
2781 } ;
2882
29- const handleStartEdit = ( provider : string , field : "apiKey" | "baseUrl" ) => {
83+ const handleStartEdit = ( provider : string , field : string , fieldConfig : FieldConfig ) => {
3084 setEditingField ( { provider, field } ) ;
31- // For API key, start empty since we only show masked value
32- // For baseUrl, show current value
33- setEditValue ( field === "baseUrl" ? ( config [ provider ] ?. baseUrl ?? "" ) : "" ) ;
85+ // For secrets, start empty since we only show masked value
86+ // For text fields, show current value
87+ const currentValue = ( config [ provider ] as Record < string , unknown > | undefined ) ?. [ field ] ;
88+ setEditValue (
89+ fieldConfig . type === "text" && typeof currentValue === "string" ? currentValue : ""
90+ ) ;
3491 } ;
3592
3693 const handleCancelEdit = ( ) => {
@@ -44,8 +101,7 @@ export function ProvidersSection() {
44101 setSaving ( true ) ;
45102 try {
46103 const { provider, field } = editingField ;
47- const keyPath = field === "apiKey" ? [ "apiKey" ] : [ "baseUrl" ] ;
48- await window . api . providers . setProviderConfig ( provider , keyPath , editValue ) ;
104+ await window . api . providers . setProviderConfig ( provider , [ field ] , editValue ) ;
49105
50106 // Refresh config
51107 const cfg = await window . api . providers . getConfig ( ) ;
@@ -57,19 +113,52 @@ export function ProvidersSection() {
57113 }
58114 } , [ editingField , editValue ] ) ;
59115
60- const handleClearBaseUrl = useCallback ( async ( provider : string ) => {
116+ const handleClearField = useCallback ( async ( provider : string , field : string ) => {
61117 setSaving ( true ) ;
62118 try {
63- await window . api . providers . setProviderConfig ( provider , [ "baseUrl" ] , "" ) ;
119+ await window . api . providers . setProviderConfig ( provider , [ field ] , "" ) ;
64120 const cfg = await window . api . providers . getConfig ( ) ;
65121 setConfig ( cfg ) ;
66122 } finally {
67123 setSaving ( false ) ;
68124 }
69125 } , [ ] ) ;
70126
71- const isConfigured = ( provider : string ) => {
72- return config [ provider ] ?. apiKeySet ?? false ;
127+ const isConfigured = ( provider : string ) : boolean => {
128+ const providerConfig = config [ provider ] ;
129+ if ( ! providerConfig ) return false ;
130+
131+ // For Bedrock, check if any credential field is set
132+ if ( provider === "bedrock" ) {
133+ return ! ! (
134+ providerConfig . region ||
135+ providerConfig . bearerTokenSet ||
136+ providerConfig . accessKeyIdSet ||
137+ providerConfig . secretAccessKeySet
138+ ) ;
139+ }
140+
141+ // For other providers, check apiKeySet
142+ return providerConfig . apiKeySet ?? false ;
143+ } ;
144+
145+ const getFieldValue = ( provider : string , field : string ) : string | undefined => {
146+ const providerConfig = config [ provider ] as Record < string , unknown > | undefined ;
147+ if ( ! providerConfig ) return undefined ;
148+ const value = providerConfig [ field ] ;
149+ return typeof value === "string" ? value : undefined ;
150+ } ;
151+
152+ const isFieldSet = ( provider : string , field : string , fieldConfig : FieldConfig ) : boolean => {
153+ if ( fieldConfig . type === "secret" ) {
154+ // For apiKey, we have apiKeySet from the sanitized config
155+ if ( field === "apiKey" ) return config [ provider ] ?. apiKeySet ?? false ;
156+ // For other secrets, check if the field exists in the raw config
157+ // Since we don't expose secret values, we assume they're not set if undefined
158+ const providerConfig = config [ provider ] as Record < string , unknown > | undefined ;
159+ return providerConfig ?. [ `${ field } Set` ] === true ;
160+ }
161+ return ! ! getFieldValue ( provider , field ) ;
73162 } ;
74163
75164 return (
@@ -81,8 +170,8 @@ export function ProvidersSection() {
81170
82171 { SUPPORTED_PROVIDERS . map ( ( provider ) => {
83172 const isExpanded = expandedProvider === provider ;
84- const providerConfig = config [ provider ] ;
85173 const configured = isConfigured ( provider ) ;
174+ const fields = getProviderFields ( provider ) ;
86175
87176 return (
88177 < div
@@ -114,117 +203,83 @@ export function ProvidersSection() {
114203 { /* Provider settings */ }
115204 { isExpanded && (
116205 < div className = "border-border-medium space-y-3 border-t px-4 py-3" >
117- { /* API Key */ }
118- < div >
119- < label className = "text-muted mb-1 block text-xs" > API Key</ label >
120- { editingField ?. provider === provider && editingField ?. field === "apiKey" ? (
121- < div className = "flex gap-2" >
122- < input
123- type = "password"
124- value = { editValue }
125- onChange = { ( e ) => setEditValue ( e . target . value ) }
126- placeholder = "Enter API key"
127- className = "bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
128- autoFocus
129- onKeyDown = { ( e ) => {
130- if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
131- if ( e . key === "Escape" ) handleCancelEdit ( ) ;
132- } }
133- />
134- < button
135- type = "button"
136- onClick = { ( ) => void handleSaveEdit ( ) }
137- disabled = { saving }
138- className = "p-1 text-green-500 hover:text-green-400"
139- >
140- < Check className = "h-4 w-4" />
141- </ button >
142- < button
143- type = "button"
144- onClick = { handleCancelEdit }
145- className = "text-muted hover:text-foreground p-1"
146- >
147- < X className = "h-4 w-4" />
148- </ button >
149- </ div >
150- ) : (
151- < div className = "flex items-center justify-between" >
152- < span className = "text-foreground font-mono text-xs" >
153- { providerConfig ?. apiKeySet ? "••••••••" : "Not set" }
154- </ span >
155- < button
156- type = "button"
157- onClick = { ( ) => handleStartEdit ( provider , "apiKey" ) }
158- className = "text-accent hover:text-accent-light text-xs"
159- >
160- { providerConfig ?. apiKeySet ? "Change" : "Set" }
161- </ button >
162- </ div >
163- ) }
164- </ div >
165-
166- { /* Base URL (optional) */ }
167- < div >
168- < label className = "text-muted mb-1 block text-xs" >
169- Base URL < span className = "text-dim" > (optional)</ span >
170- </ label >
171- { editingField ?. provider === provider && editingField ?. field === "baseUrl" ? (
172- < div className = "flex gap-2" >
173- < input
174- type = "text"
175- value = { editValue }
176- onChange = { ( e ) => setEditValue ( e . target . value ) }
177- placeholder = "https://api.example.com"
178- className = "bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
179- autoFocus
180- onKeyDown = { ( e ) => {
181- if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
182- if ( e . key === "Escape" ) handleCancelEdit ( ) ;
183- } }
184- />
185- < button
186- type = "button"
187- onClick = { ( ) => void handleSaveEdit ( ) }
188- disabled = { saving }
189- className = "p-1 text-green-500 hover:text-green-400"
190- >
191- < Check className = "h-4 w-4" />
192- </ button >
193- < button
194- type = "button"
195- onClick = { handleCancelEdit }
196- className = "text-muted hover:text-foreground p-1"
197- >
198- < X className = "h-4 w-4" />
199- </ button >
200- </ div >
201- ) : (
202- < div className = "flex items-center justify-between" >
203- < span className = "text-foreground font-mono text-xs" >
204- { providerConfig ?. baseUrl ?? "Default" }
205- </ span >
206- < div className = "flex gap-2" >
207- { providerConfig ?. baseUrl && (
206+ { fields . map ( ( fieldConfig ) => {
207+ const isEditing =
208+ editingField ?. provider === provider && editingField ?. field === fieldConfig . key ;
209+ const fieldValue = getFieldValue ( provider , fieldConfig . key ) ;
210+ const fieldIsSet = isFieldSet ( provider , fieldConfig . key , fieldConfig ) ;
211+
212+ return (
213+ < div key = { fieldConfig . key } >
214+ < label className = "text-muted mb-1 block text-xs" >
215+ { fieldConfig . label }
216+ { fieldConfig . optional && < span className = "text-dim" > (optional)</ span > }
217+ </ label >
218+ { isEditing ? (
219+ < div className = "flex gap-2" >
220+ < input
221+ type = { fieldConfig . type === "secret" ? "password" : "text" }
222+ value = { editValue }
223+ onChange = { ( e ) => setEditValue ( e . target . value ) }
224+ placeholder = { fieldConfig . placeholder }
225+ className = "bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
226+ autoFocus
227+ onKeyDown = { ( e ) => {
228+ if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
229+ if ( e . key === "Escape" ) handleCancelEdit ( ) ;
230+ } }
231+ />
208232 < button
209233 type = "button"
210- onClick = { ( ) => void handleClearBaseUrl ( provider ) }
234+ onClick = { ( ) => void handleSaveEdit ( ) }
211235 disabled = { saving }
212- className = "text-muted hover:text-error text-xs"
236+ className = "p-1 text-green-500 hover:text-green-400"
237+ >
238+ < Check className = "h-4 w-4" />
239+ </ button >
240+ < button
241+ type = "button"
242+ onClick = { handleCancelEdit }
243+ className = "text-muted hover:text-foreground p-1"
213244 >
214- Clear
245+ < X className = "h-4 w-4" />
215246 </ button >
216- ) }
217- < button
218- type = "button"
219- onClick = { ( ) => handleStartEdit ( provider , "baseUrl" ) }
220- className = "text-accent hover:text-accent-light text-xs"
221- >
222- { providerConfig ?. baseUrl ? "Change" : "Set" }
223- </ button >
224- </ div >
247+ </ div >
248+ ) : (
249+ < div className = "flex items-center justify-between" >
250+ < span className = "text-foreground font-mono text-xs" >
251+ { fieldConfig . type === "secret"
252+ ? fieldIsSet
253+ ? "••••••••"
254+ : "Not set"
255+ : ( fieldValue ?? "Default" ) }
256+ </ span >
257+ < div className = "flex gap-2" >
258+ { fieldConfig . type === "text" && fieldValue && (
259+ < button
260+ type = "button"
261+ onClick = { ( ) => void handleClearField ( provider , fieldConfig . key ) }
262+ disabled = { saving }
263+ className = "text-muted hover:text-error text-xs"
264+ >
265+ Clear
266+ </ button >
267+ ) }
268+ < button
269+ type = "button"
270+ onClick = { ( ) =>
271+ handleStartEdit ( provider , fieldConfig . key , fieldConfig )
272+ }
273+ className = "text-accent hover:text-accent-light text-xs"
274+ >
275+ { fieldIsSet || fieldValue ? "Change" : "Set" }
276+ </ button >
277+ </ div >
278+ </ div >
279+ ) }
225280 </ div >
226- ) }
227- </ div >
281+ ) ;
282+ } ) }
228283 </ div >
229284 ) }
230285 </ div >
0 commit comments