11import React , { useState , useEffect , useCallback } from "react" ;
2- import { Plus , Trash2 } from "lucide-react" ;
2+ import { Plus , Trash2 , Pencil , Check , X , Loader2 } from "lucide-react" ;
33import type { ProvidersConfigMap } from "../types" ;
44import { SUPPORTED_PROVIDERS , PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers" ;
55
@@ -8,10 +8,18 @@ interface NewModelForm {
88 modelId : string ;
99}
1010
11+ interface EditingState {
12+ provider : string ;
13+ originalModelId : string ;
14+ newModelId : string ;
15+ }
16+
1117export function ModelsSection ( ) {
12- const [ config , setConfig ] = useState < ProvidersConfigMap > ( { } ) ;
18+ const [ config , setConfig ] = useState < ProvidersConfigMap | null > ( null ) ;
1319 const [ newModel , setNewModel ] = useState < NewModelForm > ( { provider : "" , modelId : "" } ) ;
1420 const [ saving , setSaving ] = useState ( false ) ;
21+ const [ editing , setEditing ] = useState < EditingState | null > ( null ) ;
22+ const [ error , setError ] = useState < string | null > ( null ) ;
1523
1624 // Load config on mount
1725 useEffect ( ( ) => {
@@ -21,26 +29,32 @@ export function ModelsSection() {
2129 } ) ( ) ;
2230 } , [ ] ) ;
2331
24- // Get all custom models across providers
25- const getAllModels = ( ) : Array < { provider : string ; modelId : string } > => {
26- const models : Array < { provider : string ; modelId : string } > = [ ] ;
27- for ( const [ provider , providerConfig ] of Object . entries ( config ) ) {
28- if ( providerConfig . models ) {
29- for ( const modelId of providerConfig . models ) {
30- models . push ( { provider, modelId } ) ;
31- }
32- }
33- }
34- return models ;
35- } ;
32+ // Check if a model already exists (for duplicate prevention)
33+ const modelExists = useCallback (
34+ ( provider : string , modelId : string , excludeOriginal ?: string ) : boolean => {
35+ if ( ! config ) return false ;
36+ const currentModels = config [ provider ] ?. models ?? [ ] ;
37+ return currentModels . some ( ( m ) => m === modelId && m !== excludeOriginal ) ;
38+ } ,
39+ [ config ]
40+ ) ;
3641
3742 const handleAddModel = useCallback ( async ( ) => {
38- if ( ! newModel . provider || ! newModel . modelId . trim ( ) ) return ;
43+ if ( ! config || ! newModel . provider || ! newModel . modelId . trim ( ) ) return ;
44+
45+ const trimmedModelId = newModel . modelId . trim ( ) ;
46+
47+ // Check for duplicates
48+ if ( modelExists ( newModel . provider , trimmedModelId ) ) {
49+ setError ( `Model "${ trimmedModelId } " already exists for this provider` ) ;
50+ return ;
51+ }
3952
53+ setError ( null ) ;
4054 setSaving ( true ) ;
4155 try {
4256 const currentModels = config [ newModel . provider ] ?. models ?? [ ] ;
43- const updatedModels = [ ...currentModels , newModel . modelId . trim ( ) ] ;
57+ const updatedModels = [ ...currentModels , trimmedModelId ] ;
4458
4559 await window . api . providers . setModels ( newModel . provider , updatedModels ) ;
4660
@@ -54,10 +68,11 @@ export function ModelsSection() {
5468 } finally {
5569 setSaving ( false ) ;
5670 }
57- } , [ newModel , config ] ) ;
71+ } , [ newModel , config , modelExists ] ) ;
5872
5973 const handleRemoveModel = useCallback (
6074 async ( provider : string , modelId : string ) => {
75+ if ( ! config ) return ;
6176 setSaving ( true ) ;
6277 try {
6378 const currentModels = config [ provider ] ?. models ?? [ ] ;
@@ -78,6 +93,78 @@ export function ModelsSection() {
7893 [ config ]
7994 ) ;
8095
96+ const handleStartEdit = useCallback ( ( provider : string , modelId : string ) => {
97+ setEditing ( { provider, originalModelId : modelId , newModelId : modelId } ) ;
98+ setError ( null ) ;
99+ } , [ ] ) ;
100+
101+ const handleCancelEdit = useCallback ( ( ) => {
102+ setEditing ( null ) ;
103+ setError ( null ) ;
104+ } , [ ] ) ;
105+
106+ const handleSaveEdit = useCallback ( async ( ) => {
107+ if ( ! config || ! editing ) return ;
108+
109+ const trimmedModelId = editing . newModelId . trim ( ) ;
110+ if ( ! trimmedModelId ) {
111+ setError ( "Model ID cannot be empty" ) ;
112+ return ;
113+ }
114+
115+ // Only validate duplicates if the model ID actually changed
116+ if ( trimmedModelId !== editing . originalModelId ) {
117+ if ( modelExists ( editing . provider , trimmedModelId ) ) {
118+ setError ( `Model "${ trimmedModelId } " already exists for this provider` ) ;
119+ return ;
120+ }
121+ }
122+
123+ setError ( null ) ;
124+ setSaving ( true ) ;
125+ try {
126+ const currentModels = config [ editing . provider ] ?. models ?? [ ] ;
127+ const updatedModels = currentModels . map ( ( m ) =>
128+ m === editing . originalModelId ? trimmedModelId : m
129+ ) ;
130+
131+ await window . api . providers . setModels ( editing . provider , updatedModels ) ;
132+
133+ // Refresh config
134+ const cfg = await window . api . providers . getConfig ( ) ;
135+ setConfig ( cfg ) ;
136+ setEditing ( null ) ;
137+
138+ // Notify other components about the change
139+ window . dispatchEvent ( new Event ( "providers-config-changed" ) ) ;
140+ } finally {
141+ setSaving ( false ) ;
142+ }
143+ } , [ editing , config , modelExists ] ) ;
144+
145+ // Show loading state while config is being fetched
146+ if ( config === null ) {
147+ return (
148+ < div className = "flex items-center justify-center gap-2 py-12" >
149+ < Loader2 className = "text-muted h-5 w-5 animate-spin" />
150+ < span className = "text-muted text-sm" > Loading settings...</ span >
151+ </ div >
152+ ) ;
153+ }
154+
155+ // Get all custom models across providers
156+ const getAllModels = ( ) : Array < { provider : string ; modelId : string } > => {
157+ const models : Array < { provider : string ; modelId : string } > = [ ] ;
158+ for ( const [ provider , providerConfig ] of Object . entries ( config ) ) {
159+ if ( providerConfig . models ) {
160+ for ( const modelId of providerConfig . models ) {
161+ models . push ( { provider, modelId } ) ;
162+ }
163+ }
164+ }
165+ return models ;
166+ } ;
167+
81168 const allModels = getAllModels ( ) ;
82169
83170 return (
@@ -122,6 +209,7 @@ export function ModelsSection() {
122209 Add
123210 </ button >
124211 </ div >
212+ { error && ! editing && < div className = "text-error mt-2 text-xs" > { error } </ div > }
125213 </ div >
126214
127215 { /* List of custom models */ }
@@ -130,29 +218,93 @@ export function ModelsSection() {
130218 < div className = "text-muted text-xs font-medium tracking-wide uppercase" >
131219 Custom Models
132220 </ div >
133- { allModels . map ( ( { provider, modelId } ) => (
134- < div
135- key = { `${ provider } -${ modelId } ` }
136- className = "border-border-medium bg-background-secondary flex items-center justify-between rounded-md border px-4 py-2"
137- >
138- < div className = "flex items-center gap-3" >
139- < span className = "text-muted text-xs" >
140- { PROVIDER_DISPLAY_NAMES [ provider as keyof typeof PROVIDER_DISPLAY_NAMES ] ??
141- provider }
142- </ span >
143- < span className = "text-foreground font-mono text-sm" > { modelId } </ span >
144- </ div >
145- < button
146- type = "button"
147- onClick = { ( ) => void handleRemoveModel ( provider , modelId ) }
148- disabled = { saving }
149- className = "text-muted hover:text-error p-1 transition-colors"
150- title = "Remove model"
221+ { allModels . map ( ( { provider, modelId } ) => {
222+ const isEditing =
223+ editing ?. provider === provider && editing ?. originalModelId === modelId ;
224+
225+ return (
226+ < div
227+ key = { `${ provider } -${ modelId } ` }
228+ className = "border-border-medium bg-background-secondary flex items-center justify-between rounded-md border px-4 py-2"
151229 >
152- < Trash2 className = "h-4 w-4" />
153- </ button >
154- </ div >
155- ) ) }
230+ < div className = "flex min-w-0 flex-1 items-center gap-3" >
231+ < span className = "text-muted shrink-0 text-xs" >
232+ { PROVIDER_DISPLAY_NAMES [ provider as keyof typeof PROVIDER_DISPLAY_NAMES ] ??
233+ provider }
234+ </ span >
235+ { isEditing ? (
236+ < div className = "flex min-w-0 flex-1 flex-col gap-1" >
237+ < input
238+ type = "text"
239+ value = { editing . newModelId }
240+ onChange = { ( e ) =>
241+ setEditing ( ( prev ) =>
242+ prev ? { ...prev , newModelId : e . target . value } : null
243+ )
244+ }
245+ onKeyDown = { ( e ) => {
246+ if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
247+ if ( e . key === "Escape" ) handleCancelEdit ( ) ;
248+ } }
249+ className = "bg-modal-bg border-border-medium focus:border-accent min-w-0 flex-1 rounded border px-2 py-1 font-mono text-xs focus:outline-none"
250+ autoFocus
251+ />
252+ { error && < div className = "text-error text-xs" > { error } </ div > }
253+ </ div >
254+ ) : (
255+ < span className = "text-foreground min-w-0 truncate font-mono text-sm" >
256+ { modelId }
257+ </ span >
258+ ) }
259+ </ div >
260+ < div className = "ml-2 flex shrink-0 items-center gap-1" >
261+ { isEditing ? (
262+ < >
263+ < button
264+ type = "button"
265+ onClick = { ( ) => void handleSaveEdit ( ) }
266+ disabled = { saving }
267+ className = "text-accent hover:text-accent-dark p-1 transition-colors"
268+ title = "Save changes (Enter)"
269+ >
270+ < Check className = "h-4 w-4" />
271+ </ button >
272+ < button
273+ type = "button"
274+ onClick = { handleCancelEdit }
275+ disabled = { saving }
276+ className = "text-muted hover:text-foreground p-1 transition-colors"
277+ title = "Cancel (Escape)"
278+ >
279+ < X className = "h-4 w-4" />
280+ </ button >
281+ </ >
282+ ) : (
283+ < >
284+ < button
285+ type = "button"
286+ onClick = { ( ) => handleStartEdit ( provider , modelId ) }
287+ disabled = { saving || editing !== null }
288+ className = "text-muted hover:text-foreground p-1 transition-colors disabled:opacity-50"
289+ title = "Edit model"
290+ >
291+ < Pencil className = "h-4 w-4" />
292+ </ button >
293+ < button
294+ type = "button"
295+ onClick = { ( ) => void handleRemoveModel ( provider , modelId ) }
296+ disabled = { saving || editing !== null }
297+ className = "text-muted hover:text-error p-1 transition-colors disabled:opacity-50"
298+ title = "Remove model"
299+ >
300+ < Trash2 className = "h-4 w-4" />
301+ </ button >
302+ </ >
303+ ) }
304+ </ div >
305+ </ div >
306+ ) ;
307+ } ) }
156308 </ div >
157309 ) : (
158310 < div className = "text-muted py-8 text-center text-sm" >
0 commit comments