200 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|