import { useState, useMemo } from 'react'; import { useAppStore } from '@/store/app-store'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ArrowRight, Cloud, Server, Check, AlertCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { PhaseModelKey, PhaseModelEntry, ClaudeCompatibleProvider, ClaudeModelAlias, } from '@automaker/types'; import { DEFAULT_PHASE_MODELS } from '@automaker/types'; interface BulkReplaceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } // Phase display names for preview const PHASE_LABELS: Record = { enhancementModel: 'Feature Enhancement', fileDescriptionModel: 'File Descriptions', imageDescriptionModel: 'Image Descriptions', commitMessageModel: 'Commit Messages', validationModel: 'GitHub Issue Validation', specGenerationModel: 'App Specification', featureGenerationModel: 'Feature Generation', backlogPlanningModel: 'Backlog Planning', projectAnalysisModel: 'Project Analysis', suggestionsModel: 'AI Suggestions', memoryExtractionModel: 'Memory Extraction', }; const ALL_PHASES = Object.keys(PHASE_LABELS) as PhaseModelKey[]; // Claude model display names const CLAUDE_MODEL_DISPLAY: Record = { haiku: 'Claude Haiku', sonnet: 'Claude Sonnet', opus: 'Claude Opus', }; export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps) { const { phaseModels, setPhaseModel, claudeCompatibleProviders } = useAppStore(); const [selectedProvider, setSelectedProvider] = useState('anthropic'); // Get enabled providers const enabledProviders = useMemo(() => { return (claudeCompatibleProviders || []).filter((p) => p.enabled !== false); }, [claudeCompatibleProviders]); // Build provider options for the dropdown const providerOptions = useMemo(() => { const options: Array<{ id: string; name: string; isNative: boolean }> = [ { id: 'anthropic', name: 'Anthropic Direct', isNative: true }, ]; enabledProviders.forEach((provider) => { options.push({ id: provider.id, name: provider.name, isNative: false, }); }); return options; }, [enabledProviders]); // Get the selected provider config (if custom) const selectedProviderConfig = useMemo(() => { if (selectedProvider === 'anthropic') return null; return enabledProviders.find((p) => p.id === selectedProvider); }, [selectedProvider, enabledProviders]); // Get the Claude model alias from a PhaseModelEntry const getClaudeModelAlias = (entry: PhaseModelEntry): ClaudeModelAlias => { // Check if model string directly matches a Claude alias if (entry.model === 'haiku' || entry.model === 'claude-haiku') return 'haiku'; if (entry.model === 'sonnet' || entry.model === 'claude-sonnet') return 'sonnet'; if (entry.model === 'opus' || entry.model === 'claude-opus') return 'opus'; // If it's a provider model, look up the mapping if (entry.providerId) { const provider = enabledProviders.find((p) => p.id === entry.providerId); if (provider) { const model = provider.models?.find((m) => m.id === entry.model); if (model?.mapsToClaudeModel) { return model.mapsToClaudeModel; } } } // Default to sonnet return 'sonnet'; }; // Find the model from provider that maps to a specific Claude model const findModelForClaudeAlias = ( provider: ClaudeCompatibleProvider | null, claudeAlias: ClaudeModelAlias, phase: PhaseModelKey ): PhaseModelEntry => { if (!provider) { // Anthropic Direct - reset to default phase model (includes correct thinking levels) return DEFAULT_PHASE_MODELS[phase]; } // Find model that maps to this Claude alias const models = provider.models || []; const match = models.find((m) => m.mapsToClaudeModel === claudeAlias); if (match) { return { providerId: provider.id, model: match.id }; } // Fallback: use first model if no match if (models.length > 0) { return { providerId: provider.id, model: models[0].id }; } // Ultimate fallback to native Claude model return { model: claudeAlias }; }; // Generate preview of changes const preview = useMemo(() => { return ALL_PHASES.map((phase) => { const currentEntry = phaseModels[phase] ?? DEFAULT_PHASE_MODELS[phase]; const claudeAlias = getClaudeModelAlias(currentEntry); const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, phase); // Get display names const getCurrentDisplay = (): string => { if (currentEntry.providerId) { const provider = enabledProviders.find((p) => p.id === currentEntry.providerId); if (provider) { const model = provider.models?.find((m) => m.id === currentEntry.model); return model?.displayName || currentEntry.model; } } return CLAUDE_MODEL_DISPLAY[claudeAlias] || currentEntry.model; }; const getNewDisplay = (): string => { if (newEntry.providerId && selectedProviderConfig) { const model = selectedProviderConfig.models?.find((m) => m.id === newEntry.model); return model?.displayName || newEntry.model; } return CLAUDE_MODEL_DISPLAY[newEntry.model as ClaudeModelAlias] || newEntry.model; }; const isChanged = currentEntry.model !== newEntry.model || currentEntry.providerId !== newEntry.providerId || currentEntry.thinkingLevel !== newEntry.thinkingLevel; return { phase, label: PHASE_LABELS[phase], claudeAlias, currentDisplay: getCurrentDisplay(), newDisplay: getNewDisplay(), newEntry, isChanged, }; }); }, [phaseModels, selectedProviderConfig, enabledProviders]); // Count how many will change const changeCount = preview.filter((p) => p.isChanged).length; // Apply the bulk replace const handleApply = () => { preview.forEach(({ phase, newEntry, isChanged }) => { if (isChanged) { setPhaseModel(phase, newEntry); } }); onOpenChange(false); }; // Check if provider has all 3 Claude model mappings const providerModelCoverage = useMemo(() => { if (selectedProvider === 'anthropic') { return { hasHaiku: true, hasSonnet: true, hasOpus: true, complete: true }; } if (!selectedProviderConfig) { return { hasHaiku: false, hasSonnet: false, hasOpus: false, complete: false }; } const models = selectedProviderConfig.models || []; const hasHaiku = models.some((m) => m.mapsToClaudeModel === 'haiku'); const hasSonnet = models.some((m) => m.mapsToClaudeModel === 'sonnet'); const hasOpus = models.some((m) => m.mapsToClaudeModel === 'opus'); return { hasHaiku, hasSonnet, hasOpus, complete: hasHaiku && hasSonnet && hasOpus }; }, [selectedProvider, selectedProviderConfig]); const providerHasModels = selectedProvider === 'anthropic' || (selectedProviderConfig && selectedProviderConfig.models?.length > 0); return ( Bulk Replace Models Switch all phase models to equivalents from a specific provider. Models are matched by their Claude model mapping (Haiku, Sonnet, Opus).
{/* Provider selector */}
{/* Warning if provider has no models */} {!providerHasModels && (
This provider has no models configured.
)} {/* Warning if provider doesn't have all 3 mappings */} {providerHasModels && !providerModelCoverage.complete && (
This provider is missing mappings for:{' '} {[ !providerModelCoverage.hasHaiku && 'Haiku', !providerModelCoverage.hasSonnet && 'Sonnet', !providerModelCoverage.hasOpus && 'Opus', ] .filter(Boolean) .join(', ')}
)} {/* Preview of changes */} {providerHasModels && (
{changeCount} of {ALL_PHASES.length} will change
{preview.map(({ phase, label, currentDisplay, newDisplay, isChanged }) => ( ))}
Phase Current New
{label} {currentDisplay} {isChanged ? ( ) : ( )} {newDisplay}
)}
); }