"use client"; import { useState, useTransition } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { PlusIcon, Trash2Icon } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ChipInput } from "@/components/chip-input"; import { SaveIndicator } from "@/components/save-indicator"; import { useAutosave } from "@/hooks/use-autosave"; import { cn } from "@/lib/utils"; import { KEYWORD_POLICIES, POLICY_BLURB, POLICY_LABEL, type KeywordPolicy, } from "@/lib/keyword-policy"; import { createTransferableAction, deleteTransferableAction, saveProfileAction, updateTransferableAction, } from "@/server/actions"; /** * How close to a posting's words a document may get, and what transfers. * * Applicant tracking systems screen on exact tokens, so a person who has run * Salesforce for three years gets cut over HubSpot — a tool they could be * productive in inside a week. Three named levels rather than a slider, because * a number between 1 and 10 tells an assistant nothing it can act on, and * because each level has to say in words what it permits. * * The transfers below are what makes the top level safe to use: a recorded * bridge is one the PERSON stands behind and can answer for in a room. Nothing * is inferred, and a bridge they have not written down does not exist. */ type Row = { id: string; have: string; covers: string[]; note: string }; export function KeywordPolicyPanel({ policy: initial, transferables, }: { policy: KeywordPolicy; transferables: Row[]; }) { const [policy, setPolicy] = useState(initial); // Removed rows are hidden locally until the revalidate catches up, the same // way the extras panel does it — the list itself comes from the server so an // add and a refresh cannot disagree about what exists. const [removed, setRemoved] = useState>(new Set()); const [pending, startTransition] = useTransition(); const rows = transferables.filter((row) => !removed.has(row.id)); const choose = (next: KeywordPolicy) => { setPolicy(next); startTransition(async () => { await saveProfileAction({ keywordPolicy: next }); }); }; const add = () => { startTransition(async () => { // A placeholder rather than an empty string: createTransferable refuses a // nameless transfer, and it should — a tool that can file one is a tool // that can file nothing at all. "New group" is the same shape the extras // panel uses beside this one. await createTransferableAction({ have: "New tool", covers: [], note: "" }); toast.success("Added. Say what it is and what it covers."); }); }; return ( Keywords

Screening software matches exact words. This is how close to a posting’s own wording your documents are allowed to get. None of these lets anything claim you did work somewhere you didn’t.

{KEYWORD_POLICIES.map((option) => ( ))}

Something you have actually used, and the tools it covers. Only these are offered, and only on the third setting — nothing is guessed for you.

{rows.map((row) => ( setRemoved((old) => new Set(old).add(row.id))} /> ))} {rows.length === 0 && (

Nothing yet. “Salesforce” covering “HubSpot, Pipedrive” is the shape of it.

)}
); } function TransferableRow({ row, onRemoved }: { row: Row; onRemoved: () => void }) { const [values, setValues] = useState({ have: row.have, covers: row.covers, note: row.note }); const { state, push } = useAutosave((next) => updateTransferableAction(row.id, next), ); const set = (patch: Partial) => { const next = { ...values, ...patch }; setValues(next); push(next); }; return (
set({ have: event.target.value })} placeholder="What you have used, e.g. Salesforce" className="h-9 flex-1 md:h-8" />
set({ covers })} placeholder="Covers: HubSpot, Pipedrive" /> set({ note: event.target.value })} placeholder="Why it transfers — the answer you would give if asked" className="h-9 md:h-8" />
); }