Dossier View
The Dossier View is a detailed intelligence dashboard for a single person, displaying their complete profile aggregated from all agent sources. It features a clean, mission-briefing style layout with expandable sections.Component Structure
TheDossierView.tsx component renders when a person card is clicked:
function DossierView({ personId, onClose }) {
const person = useQuery(api.persons.getById, { personId });
const intel = useQuery(api.intel.byPerson, { personId });
const connections = useQuery(api.connections.getForPerson, { personId });
if (!person) return <div>Loading...</div>;
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="dossier-container"
>
<DossierHeader person={person} onClose={onClose} />
<DossierSummary dossier={person.dossier} />
<DossierSections intel={intel} />
<DossierConnections connections={connections} />
</motion.div>
);
}
Layout Sections
Header with Photo and Name
function DossierHeader({ person, onClose }) {
return (
<div className="relative">
<button onClick={onClose} className="absolute top-4 right-4">
<X size={24} />
</button>
<div className="flex items-start gap-6 p-6">
<img
src={person.photoUrl}
alt={person.name}
className="w-32 h-32 rounded-lg object-cover border-4 border-gray-300"
/>
<div>
<h1 className="text-3xl font-bold">{person.name}</h1>
{person.dossier?.title && (
<p className="text-xl text-gray-600">{person.dossier.title}</p>
)}
{person.dossier?.company && (
<p className="text-lg text-gray-500">{person.dossier.company}</p>
)}
<div className="flex gap-2 mt-4">
<ConfidenceBadge confidence={person.confidence} />
<StatusBadge status={person.status} />
</div>
</div>
</div>
</div>
);
}
Summary Section
AI-generated 2-3 sentence summary:function DossierSummary({ dossier }) {
if (!dossier?.summary) return null;
return (
<div className="p-6 bg-gray-50 border-l-4 border-red-600">
<h2 className="text-sm font-semibold text-gray-500 uppercase mb-2">
Summary
</h2>
<p className="text-lg leading-relaxed">{dossier.summary}</p>
</div>
);
}
Work History
function WorkHistorySection({ workHistory }) {
return (
<Accordion title="Work History">
<div className="space-y-4">
{workHistory.map((job, i) => (
<div key={i} className="border-l-2 border-gray-300 pl-4">
<h3 className="font-semibold">{job.role}</h3>
<p className="text-gray-600">{job.company}</p>
{job.period && (
<p className="text-sm text-gray-500">{job.period}</p>
)}
</div>
))}
</div>
</Accordion>
);
}
Education
function EducationSection({ education }) {
return (
<Accordion title="Education">
<div className="space-y-3">
{education.map((edu, i) => (
<div key={i}>
<h3 className="font-semibold">{edu.school}</h3>
{edu.degree && (
<p className="text-gray-600">{edu.degree}</p>
)}
</div>
))}
</div>
</Accordion>
);
}
Social Profiles
function SocialProfilesSection({ socialProfiles }) {
const platforms = [
{ key: "linkedin", icon: Linkedin, label: "LinkedIn" },
{ key: "twitter", icon: Twitter, label: "Twitter" },
{ key: "instagram", icon: Instagram, label: "Instagram" },
{ key: "github", icon: Github, label: "GitHub" },
{ key: "website", icon: Globe, label: "Website" },
];
return (
<Accordion title="Social Profiles">
<div className="grid grid-cols-2 gap-3">
{platforms.map(platform => {
const url = socialProfiles[platform.key];
if (!url) return null;
return (
<a
key={platform.key}
href={url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-3 bg-white border rounded hover:bg-gray-50"
>
<platform.icon size={20} />
<span>{platform.label}</span>
</a>
);
})}
</div>
</Accordion>
);
}
Notable Activity
function NotableActivitySection({ notableActivity }) {
return (
<Accordion title="Notable Activity">
<ul className="space-y-2">
{notableActivity.map((item, i) => (
<li key={i} className="flex gap-2">
<span className="text-red-600">•</span>
<span>{item}</span>
</li>
))}
</ul>
</Accordion>
);
}
Conversation Hooks
AI-generated conversation starters:function ConversationHooksSection({ conversationHooks }) {
return (
<Accordion title="Conversation Hooks">
<div className="space-y-3">
{conversationHooks.map((hook, i) => (
<div key={i} className="p-3 bg-green-50 border-l-4 border-green-600">
<p className="text-sm font-medium">💡 {hook}</p>
</div>
))}
</div>
</Accordion>
);
}
Risk Flags
function RiskFlagsSection({ riskFlags }) {
if (!riskFlags.length) return null;
return (
<Accordion title="Risk Flags">
<div className="space-y-2">
{riskFlags.map((flag, i) => (
<div key={i} className="p-3 bg-red-50 border-l-4 border-red-600">
<p className="text-sm font-medium text-red-800">⚠️ {flag}</p>
</div>
))}
</div>
</Accordion>
);
}
Connections
Visual relationship graph:function DossierConnections({ connections }) {
return (
<div className="p-6">
<h2 className="text-lg font-semibold mb-4">Connections</h2>
<div className="grid grid-cols-2 gap-4">
{connections?.map(connection => (
<div key={connection._id} className="border rounded p-3">
<div className="flex items-center gap-2 mb-2">
<img
src={connection.personB.photoUrl}
alt={connection.personB.name}
className="w-10 h-10 rounded-full"
/>
<span className="font-medium">{connection.personB.name}</span>
</div>
<p className="text-sm text-gray-600">
{connection.relationshipType}: {connection.description}
</p>
</div>
))}
</div>
</div>
);
}
Real-Time Updates
The dossier updates as agents gather more information:useEffect(() => {
if (person?.status === "researching") {
// Show loading indicators for sections being researched
console.log("Research in progress...");
} else if (person?.status === "complete") {
// All data loaded, show complete dossier
console.log("Research complete!");
}
}, [person?.status]);
useEffect(() => {
// As new intel fragments arrive, update UI sections
console.log(`Received ${intel?.length || 0} intel fragments`);
}, [intel]);
Accordion Component
Collapsible sections using Framer Motion:function Accordion({ title, children, defaultOpen = false }) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border-b">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-4 hover:bg-gray-50"
>
<h3 className="font-semibold">{title}</h3>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown size={20} />
</motion.div>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="p-4 pt-0">{children}</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Badges and Status Indicators
function ConfidenceBadge({ confidence }) {
const percentage = Math.round(confidence * 100);
const color = confidence > 0.8 ? "green" : confidence > 0.5 ? "yellow" : "red";
return (
<span className={`px-3 py-1 rounded-full bg-${color}-100 text-${color}-800`}>
{percentage}% confidence
</span>
);
}
function StatusBadge({ status }) {
const labels = {
identified: "Identified",
researching: "Researching...",
synthesizing: "Synthesizing...",
complete: "Complete",
};
return (
<span className="px-3 py-1 rounded-full bg-blue-100 text-blue-800">
{labels[status]}
</span>
);
}
Export Dossier
Download as JSON or PDF:function ExportButton({ person }) {
const exportJSON = () => {
const json = JSON.stringify(person, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${person.name}-dossier.json`;
a.click();
};
return <button onClick={exportJSON}>Export as JSON</button>;
}
Use the accordion pattern to keep the dossier scannable. Users can quickly collapse sections they’re not interested in and focus on relevant information.
Next Steps
Convex Schema
Understand the person data structure
Synthesis Engine
Learn how dossiers are generated