All files / src/components PersonalitySelector.tsx

0% Statements 0/87
0% Branches 0/1
0% Functions 0/1
0% Lines 0/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135                                                                                                                                                                                                                                                                             
/**
 * PersonalitySelector - Personality style selection component for Agent onboarding
 *
 * Displays personality options as selectable cards with icons and descriptions.
 */
import { motion } from 'framer-motion';
import { Briefcase, Heart, Sparkles, Zap, Check } from 'lucide-react';
import { cn } from '../lib/utils';
import { PERSONALITY_OPTIONS } from '../lib/personality-presets';
 
export interface PersonalitySelectorProps {
  value?: string;
  onChange: (personalityId: string) => void;
  className?: string;
}
 
// Map icon names to components
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
  Briefcase,
  Heart,
  Sparkles,
  Zap,
};
 
export function PersonalitySelector({ value, onChange, className }: PersonalitySelectorProps) {
  return (
    <div className={cn('grid grid-cols-2 gap-3', className)}>
      {PERSONALITY_OPTIONS.map((option) => {
        const IconComponent = iconMap[option.icon] || Briefcase;
        const isSelected = value === option.id;
 
        return (
          <motion.button
            key={option.id}
            type="button"
            whileHover={{ scale: 1.02 }}
            whileTap={{ scale: 0.98 }}
            onClick={() => onChange(option.id)}
            className={cn(
              'relative p-4 rounded-xl border-2 text-left transition-all',
              'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
              isSelected
                ? 'border-primary bg-primary/5 dark:bg-primary/10'
                : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 bg-white dark:bg-gray-800'
            )}
          >
            {/* Selection indicator */}
            {isSelected && (
              <motion.div
                initial={{ scale: 0 }}
                animate={{ scale: 1 }}
                className="absolute top-2 right-2 w-5 h-5 bg-primary rounded-full flex items-center justify-center"
              >
                <Check className="w-3 h-3 text-white" />
              </motion.div>
            )}
 
            {/* Icon */}
            <div
              className={cn(
                'w-10 h-10 rounded-lg flex items-center justify-center mb-3',
                isSelected
                  ? 'bg-primary text-white'
                  : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
              )}
            >
              <IconComponent className="w-5 h-5" />
            </div>
 
            {/* Label */}
            <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">
              {option.label}
            </h4>
 
            {/* Description */}
            <p className="text-xs text-gray-500 dark:text-gray-400">
              {option.description}
            </p>
 
            {/* Traits preview */}
            <div className="mt-3 flex flex-wrap gap-1">
              {option.traits.slice(0, 2).map((trait) => (
                <span
                  key={trait}
                  className={cn(
                    'px-2 py-0.5 text-xs rounded-full',
                    isSelected
                      ? 'bg-primary/10 text-primary'
                      : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
                  )}
                >
                  {trait}
                </span>
              ))}
            </div>
          </motion.button>
        );
      })}
    </div>
  );
}
 
// Export a compact version for display purposes
export interface PersonalityBadgeProps {
  personalityId?: string;
  size?: 'sm' | 'md';
  className?: string;
}
 
export function PersonalityBadge({ personalityId, size = 'sm', className }: PersonalityBadgeProps) {
  if (!personalityId) return null;
 
  const option = PERSONALITY_OPTIONS.find((p) => p.id === personalityId);
  if (!option) return null;
 
  const IconComponent = iconMap[option.icon] || Briefcase;
  const sizeStyles = {
    sm: 'px-2 py-1 text-xs',
    md: 'px-3 py-1.5 text-sm',
  };
 
  return (
    <span
      className={cn(
        'inline-flex items-center gap-1 rounded-full bg-primary/10 text-primary',
        sizeStyles[size],
        className
      )}
    >
      <IconComponent className={size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'} />
      <span>{option.label}</span>
    </span>
  );
}