feat(ui): enhance model select modal UX and modal traffic lights (#1111)
* feat(model-select-modal): highlight added models and support bulk selection - Add addedModelValues prop to highlight already-added models with primary color - Sort models alphabetically per provider, with added models floated to top - Replace green highlight with primary brand color (orange #E56A4A) - Use check icon (10px) inline with model name instead of check_circle - Replace Done button with info bar explaining click-to-toggle behavior - Add ProviderIcon to provider group headers replacing colored dot - Import ProviderIcon, remove unused Button import * feat(cli-tools): wire addedModelValues, onDeselect, and auto-save to model select modals - Pass selectedModels as addedModelValues to ModelSelectModal in OpenCode and Copilot cards - Add onDeselect handler to remove model from list on second click - Set closeOnSelect=false to allow bulk model selection - Remove manual setModalOpen(false) from onSelect callbacks - Add saveModels() silent auto-save triggered on modal close (OpenCodeToolCard) - Use useRef to track latest selectedModels in closure-safe way * feat(modal): functional traffic light close button with hover icon and tooltip - Make red dot a clickable button that closes the modal - Show ✕ icon inside red dot on hover via group-hover opacity transition - Gray out yellow and green dots (cursor-not-allowed, no tooltip) - Increase dot size from w-3 h-3 to w-4 h-4 - Add Tooltip with brand-matched color #FF5F56 on red dot - Remove X close button from modal header * feat(tooltip): add color prop for themed tooltip backgrounds * feat(i18n): add translations for model select info bar and close tooltip - Add 'Click to add, click again to remove. Changes are saved automatically.' to all 32 locales - Add 'Close' translation to all 32 locales * fix(ui): address code review feedback on modal UX and auto-save - Modal: remove showCloseButton prop, use showTrafficLights for header condition, hide traffic lights on mobile (hidden md:flex), add mobile X button (md:hidden) with aria-label, add aria-label and title on traffic light close button - OpenCodeToolCard: validate activeModel membership before saving — fallback to models[0] or empty string; clear/reassign activeModel on deselect when removed model was the active one - CopilotToolCard: add useRef + selectedModelsRef, add saveModels() using /api/cli-tools/copilot-settings, wire auto-save on modal close - ModelSelectModal: fix JSX formatting — separate info bar closing div from Search comment onto its own line
This commit is contained in:
committed by
GitHub
parent
4098f91ac5
commit
1fd3132647
@@ -3,7 +3,7 @@
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Modal from "./Modal";
|
||||
import Button from "./Button";
|
||||
import ProviderIcon from "./ProviderIcon";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, AI_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, getProviderAlias } from "@/shared/constants/providers";
|
||||
|
||||
@@ -318,32 +318,37 @@ export default function ModelSelectModal({
|
||||
return combos.filter(c => c.name.toLowerCase().includes(query));
|
||||
}, [combos, searchQuery, kindFilter]);
|
||||
|
||||
// Sort models alphabetically, with added models floated to top
|
||||
const sortModels = (models) => {
|
||||
const added = models.filter(m => addedModelValues.includes(m.value)).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const rest = models.filter(m => !addedModelValues.includes(m.value)).sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [...added, ...rest];
|
||||
};
|
||||
|
||||
// Filter models by search query
|
||||
const filteredGroups = useMemo(() => {
|
||||
if (!searchQuery.trim()) return groupedModels;
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
const filtered = {};
|
||||
|
||||
Object.entries(groupedModels).forEach(([providerId, group]) => {
|
||||
const matchedModels = group.models.filter(
|
||||
(m) =>
|
||||
m.name.toLowerCase().includes(query) ||
|
||||
m.id.toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
const providerNameMatches = group.name.toLowerCase().includes(query);
|
||||
|
||||
if (matchedModels.length > 0 || providerNameMatches) {
|
||||
filtered[providerId] = {
|
||||
...group,
|
||||
models: matchedModels,
|
||||
};
|
||||
let models = group.models;
|
||||
if (query) {
|
||||
const providerNameMatches = group.name.toLowerCase().includes(query);
|
||||
models = models.filter(
|
||||
(m) =>
|
||||
m.name.toLowerCase().includes(query) ||
|
||||
m.id.toLowerCase().includes(query)
|
||||
);
|
||||
if (models.length === 0 && !providerNameMatches) return;
|
||||
}
|
||||
filtered[providerId] = {
|
||||
...group,
|
||||
models: sortModels(models),
|
||||
};
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [groupedModels, searchQuery]);
|
||||
}, [groupedModels, searchQuery, addedModelValues]);
|
||||
|
||||
const handleSelect = (model) => {
|
||||
const value = model?.value || model?.name || model;
|
||||
@@ -371,20 +376,14 @@ export default function ModelSelectModal({
|
||||
title={title}
|
||||
size="md"
|
||||
className="p-4!"
|
||||
footer={
|
||||
!closeOnSelect ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setSearchQuery("");
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
footer={null}
|
||||
>
|
||||
{/* Info bar */}
|
||||
<div className="flex items-center gap-2 mb-3 px-2.5 py-2 bg-primary/8 border border-primary/20 rounded-lg text-xs text-text-muted">
|
||||
<span className="material-symbols-outlined text-primary shrink-0" style={{ fontSize: "14px" }}>info</span>
|
||||
<span>Click to add, click again to remove. Changes are saved automatically.</span>
|
||||
</div>
|
||||
|
||||
{/* Search - compact */}
|
||||
<div className="mb-3">
|
||||
<div className="relative">
|
||||
@@ -423,13 +422,13 @@ export default function ModelSelectModal({
|
||||
${isSelected
|
||||
? "bg-primary text-white border-primary"
|
||||
: 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-primary border-primary text-white hover:bg-primary-hover"
|
||||
: "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>
|
||||
<span className="material-symbols-outlined leading-none" style={{ fontSize: "10px" }}>check</span>
|
||||
)}
|
||||
{combo.name}
|
||||
</button>
|
||||
@@ -444,9 +443,12 @@ export default function ModelSelectModal({
|
||||
<div key={providerId}>
|
||||
{/* Provider header */}
|
||||
<div className="flex items-center gap-1.5 mb-1.5 sticky top-0 bg-surface py-0.5">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: group.color }}
|
||||
<ProviderIcon
|
||||
src={`/providers/${providerId}.png`}
|
||||
alt={group.name}
|
||||
size={14}
|
||||
fallbackText={(group.name || providerId).slice(0, 2).toUpperCase()}
|
||||
fallbackColor={group.color}
|
||||
/>
|
||||
<span className="text-xs font-medium text-primary">
|
||||
{group.name}
|
||||
@@ -472,14 +474,14 @@ export default function ModelSelectModal({
|
||||
: isSelected
|
||||
? "bg-primary text-white border-primary"
|
||||
: 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-primary border-primary text-white hover:bg-primary-hover"
|
||||
: "bg-surface border-border text-text-main hover:border-primary/50 hover:bg-primary/5"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{addedModelValues.includes(model.value) && !isPlaceholder && (
|
||||
<span className="material-symbols-outlined text-[12px]">check_circle</span>
|
||||
<span className="material-symbols-outlined leading-none" style={{ fontSize: "10px" }}>check</span>
|
||||
)}
|
||||
{isPlaceholder ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user