From 0667a26b5adf9720f4ff17543ae1b81eff84b5d1 Mon Sep 17 00:00:00 2001 From: Fajar Hidayat Date: Thu, 7 May 2026 15:55:43 +0700 Subject: [PATCH] feat: add model deselection functionality in ComboFormModal and ComboDetailPage (#889) - Implemented handleDeselectModel function to allow users to deselect models in both ComboFormModal and ComboDetailPage. - Updated ModelSelectModal to handle deselection and visually indicate selected models. - Enhanced user experience by allowing models to be removed from the selection without closing the modal. --- src/app/(dashboard)/dashboard/combos/page.js | 7 ++ .../media-providers/combo/[id]/page.js | 11 ++++ src/shared/components/ModelSelectModal.js | 64 ++++++++++++++----- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index e5b5b36..bf21475 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -390,6 +390,10 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF } }; + const handleDeselectModel = (model) => { + setModels(models.filter((m) => m !== model.value)); + }; + const handleRemoveModel = (index) => { setModels(models.filter((_, i) => i !== index)); }; @@ -502,10 +506,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF isOpen={showModelSelect} onClose={() => setShowModelSelect(false)} onSelect={handleAddModel} + onDeselect={handleDeselectModel} activeProviders={activeProviders} modelAliases={modelAliases} title="Add Model to Combo" kindFilter={kindFilter} + addedModelValues={models} + closeOnSelect={false} /> ); diff --git a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js index e4f6aad..45c1546 100644 --- a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js @@ -126,6 +126,14 @@ export default function ComboDetailPage() { await saveCombo({ models: next }); }; + const handleDeselectModel = async (model) => { + const value = model?.value || model; + if (!value || !providers.includes(value)) return; + const next = providers.filter((p) => p !== value); + setProviders(next); + await saveCombo({ models: next }); + }; + const handleRemoveProvider = async (idx) => { const next = providers.filter((_, i) => i !== idx); setProviders(next); @@ -389,10 +397,13 @@ export default function ComboDetailPage() { isOpen={showPicker} onClose={() => setShowPicker(false)} onSelect={handleAddModel} + onDeselect={handleDeselectModel} activeProviders={connections} modelAliases={modelAliases} title={`Add ${kindLabel} Model`} kindFilter={combo.kind} + addedModelValues={providers} + closeOnSelect={false} /> ); diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index 051b30d..3d364b8 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -21,11 +21,14 @@ export default function ModelSelectModal({ isOpen, onClose, onSelect, + onDeselect, selectedModel, activeProviders = [], title = "Select Model", modelAliases = {}, kindFilter = null, + addedModelValues = [], + closeOnSelect = true, }) { // Filter activeProviders by serviceKinds when kindFilter set (e.g. "webSearch", "webFetch") const filteredActiveProviders = useMemo(() => { @@ -342,9 +345,19 @@ export default function ModelSelectModal({ }, [groupedModels, searchQuery]); const handleSelect = (model) => { - onSelect(model); - onClose(); - setSearchQuery(""); + const value = model?.value || model?.name || model; + const isAdded = addedModelValues.includes(value); + + if (isAdded && onDeselect) { + onDeselect(model); + } else { + onSelect(model); + } + + if (closeOnSelect) { + onClose(); + setSearchQuery(""); + } }; return ( @@ -392,13 +405,18 @@ export default function ModelSelectModal({ key={combo.id} onClick={() => handleSelect({ id: combo.name, name: combo.name, value: combo.name })} className={` - px-2 py-1 rounded-xl text-xs font-medium transition-all border hover:cursor-pointer + px-2 py-1 rounded-xl text-xs font-medium transition-all border hover:cursor-pointer flex items-center gap-1 ${isSelected ? "bg-primary text-white border-primary" - : "bg-surface border-border text-text-main hover:border-primary/50 hover:bg-primary/5" + : addedModelValues.includes(combo.name) + ? "bg-green-500/10 border-green-500/30 text-green-700 dark:text-green-400 hover:border-green-500/50" + : "bg-surface border-border text-text-main hover:border-primary/50 hover:bg-primary/5" } `} > + {addedModelValues.includes(combo.name) && ( + check_circle + )} {combo.name} ); @@ -439,21 +457,30 @@ export default function ModelSelectModal({ ? "border-dashed border-border text-text-muted hover:border-primary/50 hover:text-primary bg-surface italic" : isSelected ? "bg-primary text-white border-primary" - : "bg-surface border-border text-text-main hover:border-primary/50 hover:bg-primary/5" + : addedModelValues.includes(model.value) + ? "bg-green-500/10 border-green-500/30 text-green-700 dark:text-green-400 hover:border-green-500/50" + : "bg-surface border-border text-text-main hover:border-primary/50 hover:bg-primary/5" } `} > - {isPlaceholder ? ( - - edit - {model.name} - - ) : model.isCustom ? ( - - {model.name} - custom - - ) : model.name} + + {addedModelValues.includes(model.value) && !isPlaceholder && ( + check_circle + )} + {isPlaceholder ? ( + <> + edit + {model.name} + + ) : model.isCustom ? ( + <> + {model.name} + custom + + ) : ( + model.name + )} + ); })} @@ -478,6 +505,7 @@ ModelSelectModal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired, + onDeselect: PropTypes.func, selectedModel: PropTypes.string, activeProviders: PropTypes.arrayOf( PropTypes.shape({ @@ -487,5 +515,7 @@ ModelSelectModal.propTypes = { title: PropTypes.string, modelAliases: PropTypes.object, kindFilter: PropTypes.string, + addedModelValues: PropTypes.arrayOf(PropTypes.string), + closeOnSelect: PropTypes.bool, };