"use client"; import { useState, useTransition } from "react"; import Link from "next/link"; import { ArrowRightIcon, BriefcaseIcon, CircleHelpIcon, DownloadIcon, LockIcon, MapPinIcon, MessageSquareWarningIcon, PlusIcon, SparklesIcon, } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { EmptyState } from "@/components/page-header"; import { Lift, Stagger, StaggerItem } from "@/components/motion"; import { useAsk } from "@/components/assistant/ask"; import { cn, dateRange, truncate } from "@/lib/utils"; import { GROUP_LABEL, GROUP_ORDER, roleGroup } from "@/lib/role-groups"; import { resolveOpenQuestionAction } from "@/server/actions"; type RoleCard = { id: string; company: string; title: string; location: string; employmentType: string; startDate: string; endDate: string; isCurrent: boolean; summary: string; tags: string[]; backgroundLength: number; highlightCount: number; /** An evidence-only preview, for a role with no scope summary. */ excerpt: string; rules: number; caveats: number; open: number; /** Days since anything was written into the background. */ daysSinceAdded: number; }; type OpenQuestion = { roleId: string | null; role: string; question: string; kind: "background" | "start_date" | "end_date"; }; /** A current job with nothing added for this long gets a nudge on its card. */ const STALE_DAYS = 30; export function RolesPanel({ roles, openQuestions, }: { roles: RoleCard[]; openQuestions: OpenQuestion[]; }) { if (roles.length === 0) { return ( } /> ); } // Grouped by kind of role when there is more than one kind: three advisory // seats listed between two jobs read as job-hopping; under their own // heading they read as what they are. const groups = GROUP_ORDER.map((group) => ({ group, roles: roles.filter((role) => roleGroup(role.employmentType) === group), })).filter((entry) => entry.roles.length > 0); return (
{openQuestions.length > 0 && } {groups.map(({ group, roles: members }) => (
{groups.length > 1 && (

{GROUP_LABEL[group]} {members.length}

)}
))}
); } function RoleGrid({ roles }: { roles: RoleCard[] }) { const ask = useAsk(); return ( {roles.map((role) => { const stale = role.isCurrent && role.daysSinceAdded >= STALE_DAYS; return (
{role.title}
{role.company}
{role.isCurrent && ( Current )}
{dateRange(role.startDate, role.endDate, role.isCurrent)} {role.location && ( {role.location} )}
{(role.summary || role.excerpt) && (

{truncate(role.summary || role.excerpt, 130)}

)} {role.tags.length > 0 && (
{role.tags.slice(0, 4).map((tag) => ( {tag} ))}
)} {stale && ( // The job you are in is the one whose details are // freshest now and gone by the time a resume is due.
Nothing added in {Math.floor(role.daysSinceAdded / 7)} weeks.
)}
{role.highlightCount} {role.highlightCount === 1 ? "highlight" : "highlights"} {formatBackground(role.backgroundLength)} {/* What the background carries besides evidence, so a rule or an unsettled fact shows from the list rather than only once you open the role. */}
); })}
); } function Marks({ role }: { role: RoleCard }) { const marks = [ { count: role.rules, icon: LockIcon, key: "rules", className: "text-muted-foreground" }, { count: role.caveats, icon: MessageSquareWarningIcon, key: "caveats", className: "text-muted-foreground" }, { count: role.open, icon: CircleHelpIcon, key: "open", className: "text-warning" }, ].filter((mark) => mark.count > 0); if (marks.length === 0) return null; const title: Record string> = { rules: (n) => `${n} ${n === 1 ? "rule" : "rules"} on how this job is described`, caveats: (n) => `${n} ${n === 1 ? "caveat" : "caveats"}, never written into a document`, open: (n) => `${n} not settled yet, kept off documents until you answer`, }; return ( {marks.map(({ count, icon: Icon, key, className }) => ( {count} {key === "open" && " open"} ))} ); } /** * What they have marked as not settled, across every role and the profile, * with the answer box right there. Answering puts the fact in as evidence * where the doubt was; a date is confirmed or corrected; "Drop" is for a * question that no longer matters. resolve_open_question is the same write. */ function OpenQuestions({ questions }: { questions: OpenQuestion[] }) { const [shownAll, setShownAll] = useState(false); const shown = shownAll ? questions : questions.slice(0, 6); return (

{questions.length === 1 ? "One thing to settle" : `${questions.length} things to settle`}

Kept off every document until you answer. Your answer goes in where the question was.

    {shown.map((item) => ( ))}
{questions.length > 6 && ( )}
); } function QuestionRow({ item }: { item: OpenQuestion }) { const [answering, setAnswering] = useState(false); const [answer, setAnswer] = useState(""); const [pending, startTransition] = useTransition(); const [gone, setGone] = useState(false); const isDate = item.kind !== "background"; const settle = (reply?: string) => startTransition(async () => { try { await resolveOpenQuestionAction(item.roleId, item.question, reply); setGone(true); toast.success(isDate ? "Date confirmed." : reply ? "Answered and filed." : "Dropped."); } catch (error) { toast.error(error instanceof Error ? error.message : "Could not save that."); } }); if (gone) return null; return (
  • {item.roleId ? ( {item.role} ) : ( Profile )} {truncate(item.question, 200)} {!answering && ( {isDate ? ( <> ) : ( <> )} )}
    {answering && (
    { event.preventDefault(); if (answer.trim()) settle(answer.trim()); }} > setAnswer(event.target.value)} placeholder={isDate ? "" : "The answer, written as a fact: “The budget was $40K a month.”"} className="h-9 md:h-8" />
    )}
  • ); } function formatBackground(length: number) { if (length === 0) return "nothing written yet"; const words = Math.round(length / 5.5); if (words < 1000) return `${words} words`; return `${(words / 1000).toFixed(1)}k words`; }