semblance-dev/src/components/UserCard.tsx

200 lines
7.1 KiB
TypeScript

import { User, ArrowLeft, Zap, Brain, Star, Tag, Award, Check, Plus } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useNavigate } from 'react-router-dom';
import { Persona } from '@/types/persona';
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Brain, Heart, Activity, Target, Zap, Users, MapPin, Edit } from 'lucide-react';
import { ResponsiveContainer, Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis } from 'recharts';
import { useState } from 'react';
import { toast } from 'sonner';
import PersonaEditor from './persona/PersonaEditor';
import { getPersonaAvatarSrc } from '@/utils/avatarUtils';
interface UserCardProps {
user: Persona;
selected?: boolean;
onClick?: (e: React.MouseEvent) => void;
showDetailedDialog?: boolean;
onSelectionToggle?: (e: React.MouseEvent) => void;
showAddToFolderButton?: boolean;
onAddToFolder?: (e: React.MouseEvent) => void;
onViewDetails?: (persona: Persona) => void;
}
export default function UserCard({
user,
selected = false,
onClick,
showDetailedDialog = false,
onSelectionToggle,
showAddToFolderButton = false,
onAddToFolder,
onViewDetails
}: UserCardProps) {
const navigate = useNavigate();
const [isEditing, setIsEditing] = useState(false);
const [currentPersona, setCurrentPersona] = useState<Persona>(user);
// Use MongoDB _id if available, otherwise fall back to id
const personaId = user._id || user.id;
const handleClick = (e: React.MouseEvent) => {
if (onClick) {
onClick(e);
} else {
navigate(`/synthetic-users/${personaId}`);
}
};
const handleViewDetails = (e: React.MouseEvent) => {
e.stopPropagation();
navigate(`/synthetic-users/${personaId}`);
};
const handleSaveEdit = (updatedPersona: Persona) => {
setCurrentPersona(updatedPersona);
setIsEditing(false);
toast.success("Persona updated successfully");
// In a real app, you would save this to your backend/state management
};
const oceanData = currentPersona.oceanTraits ? [
{ trait: 'Openness', value: currentPersona.oceanTraits.openness || 50 },
{ trait: 'Conscientiousness', value: currentPersona.oceanTraits.conscientiousness || 50 },
{ trait: 'Extraversion', value: currentPersona.oceanTraits.extraversion || 50 },
{ trait: 'Agreeableness', value: currentPersona.oceanTraits.agreeableness || 50 },
{ trait: 'Neuroticism', value: currentPersona.oceanTraits.neuroticism || 50 },
] : [];
const handleCardClick = (e: React.MouseEvent) => {
// Check if the click target is the "View Details" button or its children
const target = e.target as HTMLElement;
const isViewDetailsButton = target.closest('button') && target.closest('button')?.textContent?.includes('View Details');
if (isViewDetailsButton) {
// Let the button handle its own click
return;
}
if (onSelectionToggle) {
onSelectionToggle(e);
} else if (onClick) {
onClick(e);
}
};
const handleViewDetailsClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (onViewDetails) {
onViewDetails(currentPersona);
} else {
handleViewDetails(e);
}
};
return (
<div
className={cn(
"persona-card glass-card rounded-xl p-4 cursor-pointer hover:shadow-md button-transition",
selected && "selected ring-2 ring-primary",
)}
onClick={handleCardClick}
>
{/* Overlay for visual feedback */}
<div className="persona-card-overlay" />
{/* Selection checkmark */}
<div className="persona-card-checkmark">
<Check className="h-4 w-4 text-primary" />
</div>
<div className="relative z-10">
<div className="flex items-start space-x-4">
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center">
<img
src={getPersonaAvatarSrc(currentPersona)}
alt={`${currentPersona.name} avatar`}
className="h-12 w-12 rounded-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
{/* Basic Demographics */}
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium truncate flex-1">{currentPersona.name}</h3>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-1">
{currentPersona.age} {currentPersona.gender}
</p>
<p className="text-xs text-muted-foreground mt-1">{currentPersona.occupation}</p>
<p className="text-xs text-muted-foreground">{currentPersona.location}</p>
{/* AI-Synthesized Bio */}
<div className="mt-2">
{currentPersona.aiSynthesizedBio ? (
<p className="text-xs text-slate-700 line-clamp-3 leading-relaxed">
{currentPersona.aiSynthesizedBio}
{currentPersona.aiSynthesizedBio.length > 150 && '...'}
</p>
) : (
<p className="text-xs text-muted-foreground italic line-clamp-3">
"{currentPersona.personality}"
</p>
)}
</div>
{/* Qualitative Attributes */}
{currentPersona.qualitativeAttributes && currentPersona.qualitativeAttributes.length > 0 && (
<div className="mt-3">
<div className="flex flex-wrap gap-1">
{currentPersona.qualitativeAttributes.slice(0, 3).map((attribute, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full"
>
<Tag className="h-3 w-3" />
{attribute}
</span>
))}
</div>
</div>
)}
{/* Top Personality Traits */}
{currentPersona.topPersonalityTraits && currentPersona.topPersonalityTraits.length > 0 && (
<div className="mt-2">
<div className="flex flex-wrap gap-1">
{currentPersona.topPersonalityTraits.slice(0, 3).map((trait, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full"
>
<Brain className="h-3 w-3" />
{trait}
</span>
))}
</div>
</div>
)}
<div className="mt-3 flex justify-end">
<Button
variant="ghost"
size="sm"
onClick={handleViewDetailsClick}
>
View Details
</Button>
</div>
</div>
</div>
</div>
</div>
);
}