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.
This commit is contained in:
Fajar Hidayat
2026-05-07 15:55:43 +07:00
committed by GitHub
parent 7f93df3a92
commit 0667a26b5a
3 changed files with 65 additions and 17 deletions

View File

@@ -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}
/>
</>
);

View File

@@ -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}
/>
</div>
);

View File

@@ -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) && (
<span className="material-symbols-outlined text-[12px]">check_circle</span>
)}
{combo.name}
</button>
);
@@ -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 ? (
<span className="flex items-center gap-1">
<span className="material-symbols-outlined text-[11px]">edit</span>
{model.name}
</span>
) : model.isCustom ? (
<span className="flex items-center gap-1">
{model.name}
<span className="text-[9px] opacity-60 font-normal">custom</span>
</span>
) : model.name}
<span className="flex items-center gap-1">
{addedModelValues.includes(model.value) && !isPlaceholder && (
<span className="material-symbols-outlined text-[12px]">check_circle</span>
)}
{isPlaceholder ? (
<>
<span className="material-symbols-outlined text-[11px]">edit</span>
{model.name}
</>
) : model.isCustom ? (
<>
{model.name}
<span className="text-[9px] opacity-60 font-normal">custom</span>
</>
) : (
model.name
)}
</span>
</button>
);
})}
@@ -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,
};