mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
feat(phase-model-selector): enhance model selection with favorites and popover UI
- Introduced a popover for model selection, allowing users to choose from Claude and Cursor models. - Added functionality to toggle favorite models, enhancing user experience by allowing quick access to preferred options. - Updated the UI to display selected model details and improved layout for better usability. - Refactored model grouping and rendering logic for clarity and maintainability. This update improves the overall interaction with the phase model selector, making it more intuitive and user-friendly.
This commit is contained in:
@@ -1,8 +1,21 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import type { ModelAlias, CursorModelId } from '@automaker/types';
|
import type { ModelAlias, CursorModelId } from '@automaker/types';
|
||||||
import { stripProviderPrefix } from '@automaker/types';
|
import { stripProviderPrefix } from '@automaker/types';
|
||||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||||
|
import { Check, ChevronsUpDown, Star, Brain, Sparkles } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
|
||||||
interface PhaseModelSelectorProps {
|
interface PhaseModelSelectorProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -17,7 +30,8 @@ export function PhaseModelSelector({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: PhaseModelSelectorProps) {
|
}: PhaseModelSelectorProps) {
|
||||||
const { enabledCursorModels } = useAppStore();
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const { enabledCursorModels, favoriteModels, toggleFavoriteModel } = useAppStore();
|
||||||
|
|
||||||
// Filter Cursor models to only show enabled ones
|
// Filter Cursor models to only show enabled ones
|
||||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||||
@@ -25,83 +39,164 @@ export function PhaseModelSelector({
|
|||||||
return enabledCursorModels.includes(cursorId);
|
return enabledCursorModels.includes(cursorId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper to find current selected model details
|
||||||
|
const currentModel = React.useMemo(() => {
|
||||||
|
const claudeModel = CLAUDE_MODELS.find((m) => m.id === value);
|
||||||
|
if (claudeModel) return { ...claudeModel, icon: Brain };
|
||||||
|
|
||||||
|
const cursorModel = availableCursorModels.find((m) => stripProviderPrefix(m.id) === value);
|
||||||
|
if (cursorModel) return { ...cursorModel, icon: Sparkles };
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [value, availableCursorModels]);
|
||||||
|
|
||||||
|
// Group models
|
||||||
|
const { favorites, claude, cursor } = React.useMemo(() => {
|
||||||
|
const favs: typeof CLAUDE_MODELS = [];
|
||||||
|
const cModels: typeof CLAUDE_MODELS = [];
|
||||||
|
const curModels: typeof CURSOR_MODELS = [];
|
||||||
|
|
||||||
|
// Process Claude Models
|
||||||
|
CLAUDE_MODELS.forEach((model) => {
|
||||||
|
if (favoriteModels.includes(model.id)) {
|
||||||
|
favs.push(model);
|
||||||
|
} else {
|
||||||
|
cModels.push(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process Cursor Models
|
||||||
|
availableCursorModels.forEach((model) => {
|
||||||
|
if (favoriteModels.includes(model.id)) {
|
||||||
|
favs.push(model);
|
||||||
|
} else {
|
||||||
|
curModels.push(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { favorites: favs, claude: cModels, cursor: curModels };
|
||||||
|
}, [favoriteModels, availableCursorModels]);
|
||||||
|
|
||||||
|
const renderModelItem = (model: (typeof CLAUDE_MODELS)[0], type: 'claude' | 'cursor') => {
|
||||||
|
const isClaude = type === 'claude';
|
||||||
|
// For Claude, value is model.id. For Cursor, it's stripped ID.
|
||||||
|
const modelValue = isClaude ? model.id : stripProviderPrefix(model.id);
|
||||||
|
const isSelected = value === modelValue;
|
||||||
|
const isFavorite = favoriteModels.includes(model.id);
|
||||||
|
const Icon = isClaude ? Brain : Sparkles;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={model.id}
|
||||||
|
value={model.label}
|
||||||
|
onSelect={() => {
|
||||||
|
onChange(modelValue as ModelAlias | CursorModelId);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className="group flex items-center justify-between py-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 overflow-hidden">
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 shrink-0',
|
||||||
|
isSelected ? 'text-primary' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col truncate">
|
||||||
|
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
|
||||||
|
{model.label}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">{model.description}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 ml-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
|
||||||
|
isFavorite
|
||||||
|
? 'text-yellow-500 opacity-100'
|
||||||
|
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleFavoriteModel(model.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
|
||||||
|
</Button>
|
||||||
|
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-4 rounded-xl',
|
'flex items-center justify-between p-4 rounded-xl',
|
||||||
'bg-accent/20 border border-border/30',
|
'bg-accent/20 border border-border/30',
|
||||||
'hover:bg-accent/30 transition-colors'
|
'hover:bg-accent/30 transition-colors'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
{/* Label and Description */}
|
||||||
{/* Label and Description */}
|
<div className="flex-1 pr-4">
|
||||||
<div>
|
<h4 className="text-sm font-medium text-foreground">{label}</h4>
|
||||||
<h4 className="text-sm font-medium text-foreground">{label}</h4>
|
<p className="text-xs text-muted-foreground">{description}</p>
|
||||||
<p className="text-xs text-muted-foreground">{description}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Model Selection */}
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{/* Claude Models */}
|
|
||||||
{CLAUDE_MODELS.map((model) => {
|
|
||||||
const isActive = value === model.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={model.id}
|
|
||||||
onClick={() => onChange(model.id as ModelAlias)}
|
|
||||||
className={cn(
|
|
||||||
'px-3 py-1.5 rounded-lg text-xs font-medium',
|
|
||||||
'transition-all duration-150',
|
|
||||||
isActive
|
|
||||||
? ['bg-brand-500/20 text-brand-500', 'border border-brand-500/40', 'shadow-sm']
|
|
||||||
: [
|
|
||||||
'bg-accent/50 text-muted-foreground',
|
|
||||||
'border border-transparent',
|
|
||||||
'hover:bg-accent hover:text-foreground',
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{model.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Divider if there are Cursor models */}
|
|
||||||
{availableCursorModels.length > 0 && (
|
|
||||||
<div className="w-px h-6 bg-border/50 mx-1 self-center" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Cursor Models */}
|
|
||||||
{availableCursorModels.map((model) => {
|
|
||||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
|
||||||
const isActive = value === cursorId;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={model.id}
|
|
||||||
onClick={() => onChange(cursorId)}
|
|
||||||
className={cn(
|
|
||||||
'px-3 py-1.5 rounded-lg text-xs font-medium',
|
|
||||||
'transition-all duration-150',
|
|
||||||
isActive
|
|
||||||
? [
|
|
||||||
'bg-purple-500/20 text-purple-400',
|
|
||||||
'border border-purple-500/40',
|
|
||||||
'shadow-sm',
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'bg-accent/50 text-muted-foreground',
|
|
||||||
'border border-transparent',
|
|
||||||
'hover:bg-accent hover:text-foreground',
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
title={model.description}
|
|
||||||
>
|
|
||||||
{model.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Model Selection Popover */}
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-[260px] justify-between h-9 px-3 bg-background/50 border-border/50 hover:bg-background/80 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 truncate">
|
||||||
|
{currentModel?.icon && (
|
||||||
|
<currentModel.icon className="h-4 w-4 text-muted-foreground/70" />
|
||||||
|
)}
|
||||||
|
<span className="truncate text-sm">{currentModel?.label || 'Select model...'}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[320px] p-0" align="end">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search models..." />
|
||||||
|
<CommandList className="max-h-[300px]">
|
||||||
|
<CommandEmpty>No model found.</CommandEmpty>
|
||||||
|
|
||||||
|
{favorites.length > 0 && (
|
||||||
|
<>
|
||||||
|
<CommandGroup heading="Favorites">
|
||||||
|
{favorites.map((model) =>
|
||||||
|
renderModelItem(model, model.provider === 'claude' ? 'claude' : 'cursor')
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{claude.length > 0 && (
|
||||||
|
<CommandGroup heading="Claude Models">
|
||||||
|
{claude.map((model) => renderModelItem(model, 'claude'))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cursor.length > 0 && (
|
||||||
|
<CommandGroup heading="Cursor Models">
|
||||||
|
{cursor.map((model) => renderModelItem(model, 'cursor'))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,6 +490,7 @@ export interface AppState {
|
|||||||
|
|
||||||
// Phase Model Settings - per-phase AI model configuration
|
// Phase Model Settings - per-phase AI model configuration
|
||||||
phaseModels: PhaseModelConfig;
|
phaseModels: PhaseModelConfig;
|
||||||
|
favoriteModels: string[];
|
||||||
|
|
||||||
// Cursor CLI Settings (global)
|
// Cursor CLI Settings (global)
|
||||||
enabledCursorModels: CursorModelId[]; // Which Cursor models are available in feature modal
|
enabledCursorModels: CursorModelId[]; // Which Cursor models are available in feature modal
|
||||||
@@ -787,6 +788,7 @@ export interface AppActions {
|
|||||||
setPhaseModel: (phase: PhaseModelKey, model: ModelAlias | CursorModelId) => Promise<void>;
|
setPhaseModel: (phase: PhaseModelKey, model: ModelAlias | CursorModelId) => Promise<void>;
|
||||||
setPhaseModels: (models: Partial<PhaseModelConfig>) => Promise<void>;
|
setPhaseModels: (models: Partial<PhaseModelConfig>) => Promise<void>;
|
||||||
resetPhaseModels: () => Promise<void>;
|
resetPhaseModels: () => Promise<void>;
|
||||||
|
toggleFavoriteModel: (modelId: string) => void;
|
||||||
|
|
||||||
// Cursor CLI Settings actions
|
// Cursor CLI Settings actions
|
||||||
setEnabledCursorModels: (models: CursorModelId[]) => void;
|
setEnabledCursorModels: (models: CursorModelId[]) => void;
|
||||||
@@ -1007,6 +1009,7 @@ const initialState: AppState = {
|
|||||||
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
||||||
validationModel: 'opus', // Default to opus for GitHub issue validation
|
validationModel: 'opus', // Default to opus for GitHub issue validation
|
||||||
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
|
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
|
||||||
|
favoriteModels: [],
|
||||||
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
|
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
|
||||||
cursorDefaultModel: 'auto', // Default to auto selection
|
cursorDefaultModel: 'auto', // Default to auto selection
|
||||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||||
@@ -1674,6 +1677,14 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
||||||
await syncSettingsToServer();
|
await syncSettingsToServer();
|
||||||
},
|
},
|
||||||
|
toggleFavoriteModel: (modelId) => {
|
||||||
|
const current = get().favoriteModels;
|
||||||
|
if (current.includes(modelId)) {
|
||||||
|
set({ favoriteModels: current.filter((id) => id !== modelId) });
|
||||||
|
} else {
|
||||||
|
set({ favoriteModels: [...current, modelId] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Cursor CLI Settings actions
|
// Cursor CLI Settings actions
|
||||||
setEnabledCursorModels: (models) => set({ enabledCursorModels: models }),
|
setEnabledCursorModels: (models) => set({ enabledCursorModels: models }),
|
||||||
|
|||||||
Reference in New Issue
Block a user