Skip to main content

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

The DossierView.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

Build docs developers (and LLMs) love