11import React , { useState , useEffect , useCallback } from "react" ;
2- import { Plus , Trash2 } from "lucide-react" ;
2+ import { Plus , Trash2 , Pencil , Check , X } 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 ( ) {
1218 const [ config , setConfig ] = useState < ProvidersConfigMap > ( { } ) ;
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 ( ( ) => {
@@ -34,13 +42,31 @@ export function ModelsSection() {
3442 return models ;
3543 } ;
3644
45+ // Check if a model already exists (for duplicate prevention)
46+ const modelExists = useCallback (
47+ ( provider : string , modelId : string , excludeOriginal ?: string ) : boolean => {
48+ const currentModels = config [ provider ] ?. models ?? [ ] ;
49+ return currentModels . some ( ( m ) => m === modelId && m !== excludeOriginal ) ;
50+ } ,
51+ [ config ]
52+ ) ;
53+
3754 const handleAddModel = useCallback ( async ( ) => {
3855 if ( ! newModel . provider || ! newModel . modelId . trim ( ) ) return ;
3956
57+ const trimmedModelId = newModel . modelId . trim ( ) ;
58+
59+ // Check for duplicates
60+ if ( modelExists ( newModel . provider , trimmedModelId ) ) {
61+ setError ( `Model "${ trimmedModelId } " already exists for this provider` ) ;
62+ return ;
63+ }
64+
65+ setError ( null ) ;
4066 setSaving ( true ) ;
4167 try {
4268 const currentModels = config [ newModel . provider ] ?. models ?? [ ] ;
43- const updatedModels = [ ...currentModels , newModel . modelId . trim ( ) ] ;
69+ const updatedModels = [ ...currentModels , trimmedModelId ] ;
4470
4571 await window . api . providers . setModels ( newModel . provider , updatedModels ) ;
4672
@@ -54,7 +80,7 @@ export function ModelsSection() {
5480 } finally {
5581 setSaving ( false ) ;
5682 }
57- } , [ newModel , config ] ) ;
83+ } , [ newModel , config , modelExists ] ) ;
5884
5985 const handleRemoveModel = useCallback (
6086 async ( provider : string , modelId : string ) => {
@@ -78,6 +104,55 @@ export function ModelsSection() {
78104 [ config ]
79105 ) ;
80106
107+ const handleStartEdit = useCallback ( ( provider : string , modelId : string ) => {
108+ setEditing ( { provider, originalModelId : modelId , newModelId : modelId } ) ;
109+ setError ( null ) ;
110+ } , [ ] ) ;
111+
112+ const handleCancelEdit = useCallback ( ( ) => {
113+ setEditing ( null ) ;
114+ setError ( null ) ;
115+ } , [ ] ) ;
116+
117+ const handleSaveEdit = useCallback ( async ( ) => {
118+ if ( ! editing ) return ;
119+
120+ const trimmedModelId = editing . newModelId . trim ( ) ;
121+ if ( ! trimmedModelId ) {
122+ setError ( "Model ID cannot be empty" ) ;
123+ return ;
124+ }
125+
126+ // Only validate duplicates if the model ID actually changed
127+ if ( trimmedModelId !== editing . originalModelId ) {
128+ if ( modelExists ( editing . provider , trimmedModelId ) ) {
129+ setError ( `Model "${ trimmedModelId } " already exists for this provider` ) ;
130+ return ;
131+ }
132+ }
133+
134+ setError ( null ) ;
135+ setSaving ( true ) ;
136+ try {
137+ const currentModels = config [ editing . provider ] ?. models ?? [ ] ;
138+ const updatedModels = currentModels . map ( ( m ) =>
139+ m === editing . originalModelId ? trimmedModelId : m
140+ ) ;
141+
142+ await window . api . providers . setModels ( editing . provider , updatedModels ) ;
143+
144+ // Refresh config
145+ const cfg = await window . api . providers . getConfig ( ) ;
146+ setConfig ( cfg ) ;
147+ setEditing ( null ) ;
148+
149+ // Notify other components about the change
150+ window . dispatchEvent ( new Event ( "providers-config-changed" ) ) ;
151+ } finally {
152+ setSaving ( false ) ;
153+ }
154+ } , [ editing , config , modelExists ] ) ;
155+
81156 const allModels = getAllModels ( ) ;
82157
83158 return (
@@ -122,6 +197,7 @@ export function ModelsSection() {
122197 Add
123198 </ button >
124199 </ div >
200+ { error && ! editing && < div className = "text-error mt-2 text-xs" > { error } </ div > }
125201 </ div >
126202
127203 { /* List of custom models */ }
@@ -130,29 +206,93 @@ export function ModelsSection() {
130206 < div className = "text-muted text-xs font-medium tracking-wide uppercase" >
131207 Custom Models
132208 </ 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"
209+ { allModels . map ( ( { provider, modelId } ) => {
210+ const isEditing =
211+ editing ?. provider === provider && editing ?. originalModelId === modelId ;
212+
213+ return (
214+ < div
215+ key = { `${ provider } -${ modelId } ` }
216+ className = "border-border-medium bg-background-secondary flex items-center justify-between rounded-md border px-4 py-2"
151217 >
152- < Trash2 className = "h-4 w-4" />
153- </ button >
154- </ div >
155- ) ) }
218+ < div className = "flex min-w-0 flex-1 items-center gap-3" >
219+ < span className = "text-muted shrink-0 text-xs" >
220+ { PROVIDER_DISPLAY_NAMES [ provider as keyof typeof PROVIDER_DISPLAY_NAMES ] ??
221+ provider }
222+ </ span >
223+ { isEditing ? (
224+ < div className = "flex min-w-0 flex-1 flex-col gap-1" >
225+ < input
226+ type = "text"
227+ value = { editing . newModelId }
228+ onChange = { ( e ) =>
229+ setEditing ( ( prev ) =>
230+ prev ? { ...prev , newModelId : e . target . value } : null
231+ )
232+ }
233+ onKeyDown = { ( e ) => {
234+ if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
235+ if ( e . key === "Escape" ) handleCancelEdit ( ) ;
236+ } }
237+ 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"
238+ autoFocus
239+ />
240+ { error && < div className = "text-error text-xs" > { error } </ div > }
241+ </ div >
242+ ) : (
243+ < span className = "text-foreground min-w-0 truncate font-mono text-sm" >
244+ { modelId }
245+ </ span >
246+ ) }
247+ </ div >
248+ < div className = "ml-2 flex shrink-0 items-center gap-1" >
249+ { isEditing ? (
250+ < >
251+ < button
252+ type = "button"
253+ onClick = { ( ) => void handleSaveEdit ( ) }
254+ disabled = { saving }
255+ className = "text-accent hover:text-accent-dark p-1 transition-colors"
256+ title = "Save changes (Enter)"
257+ >
258+ < Check className = "h-4 w-4" />
259+ </ button >
260+ < button
261+ type = "button"
262+ onClick = { handleCancelEdit }
263+ disabled = { saving }
264+ className = "text-muted hover:text-foreground p-1 transition-colors"
265+ title = "Cancel (Escape)"
266+ >
267+ < X className = "h-4 w-4" />
268+ </ button >
269+ </ >
270+ ) : (
271+ < >
272+ < button
273+ type = "button"
274+ onClick = { ( ) => handleStartEdit ( provider , modelId ) }
275+ disabled = { saving || editing !== null }
276+ className = "text-muted hover:text-foreground p-1 transition-colors disabled:opacity-50"
277+ title = "Edit model"
278+ >
279+ < Pencil className = "h-4 w-4" />
280+ </ button >
281+ < button
282+ type = "button"
283+ onClick = { ( ) => void handleRemoveModel ( provider , modelId ) }
284+ disabled = { saving || editing !== null }
285+ className = "text-muted hover:text-error p-1 transition-colors disabled:opacity-50"
286+ title = "Remove model"
287+ >
288+ < Trash2 className = "h-4 w-4" />
289+ </ button >
290+ </ >
291+ ) }
292+ </ div >
293+ </ div >
294+ ) ;
295+ } ) }
156296 </ div >
157297 ) : (
158298 < div className = "text-muted py-8 text-center text-sm" >
0 commit comments