import type { ReactNode } from "react";
import type { CheckIn, CheckInNote } from "../../api/types";
import { isEnded, sentenceNotes } from "../../logic/checkin-events";
import { callOffset, clock, dayLabel, noteKinds, whenLabel } from "../../logic/format";
import { checkInPill } from "../../logic/pills";
import { checkClass, cn, Icon, linkButtonClass } from "../kit";
import { Pill } from "../Pill";
import { NoteIcon } from "./NoteIcon";
const refClass =
"ml-0.5 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-page px-1 align-[1px] font-mono text-[10.5px] leading-none text-cell no-underline shadow-[inset_0_0_0_1px_var(--color-line)] outline-none hover:text-ink hover:shadow-[inset_0_0_0_1px_var(--color-subtle)] focus-visible:shadow-[0_0_0_2px_var(--color-brand)]";
/** Every state the call went through, in order, with its time. */
export function StatusTrail({ call }: { call: CheckIn }) {
return (
<ol aria-label="Status" className="m-0 flex list-none flex-wrap items-center gap-x-2 gap-y-2 p-0">
{call.history.map((h, i) => (
<li key={i} className="inline-flex items-center gap-1.5">
{i > 0 && <Icon name="arrow-right-s-line" size={14} className="text-line" />}
<Pill pill={checkInPill(h.status)} size="sm" />
<small className="font-mono text-[11.5px] text-subtle">
{dayLabel(h.at) === "Today" ? clock(h.at) : whenLabel(h.at)}
{h.detail ? ` · ${h.detail.replace(/_/g, " ")}` : ""}
</small>
</li>
))}
</ol>
);
}
/** Loud only when a patient needs someone now: red, its words, and what to do next. */
export function UrgentBox({ title, children, actions, role = "alert" }: { title: ReactNode; children?: ReactNode; actions?: ReactNode; role?: "alert" | "status" }) {
return (
<div role={role} className="flex flex-wrap items-start gap-x-3.5 gap-y-3 rounded-[14px] bg-red-fill px-4 py-3.5">
<Icon name="alarm-warning-fill" size={22} className="text-red-icon" />
<div className="flex min-w-0 flex-1 basis-60 flex-col gap-1">
<strong className="text-[14.5px] leading-5 font-semibold text-[#8f1d14]">{title}</strong>
{children && <div className="flex flex-col gap-1 text-[13.5px] leading-[19px] text-[#a42418]">{children}</div>}
</div>
{actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
</div>
);
}
/** The notes as they arrive: what was said, in the patient's words, linked to the line in the call text. */
export function NotesList({
call,
first,
picked,
onPick,
onJump,
}: {
call: CheckIn;
first: string;
picked: string[];
onPick: (id: string, on: boolean) => void;
onJump: (lineIds: string[]) => void;
}) {
const at = (n: CheckInNote) => {
const line = call.transcript.find((l) => l.id === n.transcriptLineIds[0]);
return line ? `${callOffset(line.offsetSeconds)} in the call` : "In the call";
};
return (
<ol className="m-0 flex list-none flex-col p-0" aria-live="polite" aria-label="Notes">
{call.notes.map((n, i) => (
<li
key={n.id}
id={`note-${n.id}`}
className={cn("flex scroll-mt-24 items-start gap-3 py-3 shadow-[inset_0_-1px_0_var(--color-hairline)] last:shadow-none", n.urgent && "-mx-2 rounded-lg bg-red-fill/50 px-2 shadow-none")}
>
{isEnded(call.status) && !n.urgent && (
<input
type="checkbox"
className={cn(checkClass, "mt-1.5")}
aria-label={`Choose "${n.text}" to add to the check`}
checked={picked.includes(n.id) || !!n.addedToCheckAt}
disabled={!!n.addedToCheckAt}
onChange={(e) => onPick(n.id, e.target.checked)}
/>
)}
<NoteIcon kind={n.kind} />
<div className="flex min-w-0 flex-1 flex-col gap-1">
<b className="text-[14px] leading-5 font-medium text-ink">{n.text}</b>
<q className="text-[13.5px] leading-5 text-cell">{n.quote}</q>
<small className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[12px] leading-[18px] text-muted">
<span className="font-mono text-subtle">{i + 1}</span>
<span aria-hidden="true">·</span>
<span>{noteKinds[n.kind]}</span>
<span aria-hidden="true">·</span>
<button className={cn(linkButtonClass, "text-[12px] font-normal text-cell")} onClick={() => onJump(n.transcriptLineIds)}>
{at(n)}
</button>
{n.addedToCheckAt && <span className="text-green-text">· added to the check</span>}
{n.disputed && <span className="text-amber-text">· {first} said "This isn't what I said"</span>}
</small>
</div>
<small className="pt-1 text-[12px] text-subtle">{first}</small>
</li>
))}
{!call.notes.length && !isEnded(call.status) && <li className="py-6 text-center text-[13.5px] text-muted">Notes appear here as {first} answers.</li>}
</ol>
);
}
/** The summary, built only from the notes: each sentence carries the numbers of its notes. */
export function SummaryCard({ call, doctor, noteNumber }: { call: CheckIn; doctor: string; noteNumber: (id: string) => number }) {
if (!call.summary) return null;
return (
<div className="flex flex-col gap-2 rounded-xl bg-tile p-4">
<b className="text-[14px] leading-5 font-semibold text-ink">Summary for {doctor}</b>
<p className="m-0 text-[14px] leading-[22px] text-body">
{call.summary.sentences.map((s, i) => (
<span key={i}>
{s.text}
{sentenceNotes(call, s.noteIds).map((n) => (
<a key={n.id} className={refClass} href={`#note-${n.id}`} aria-label={`Note ${noteNumber(n.id)}: ${n.text}`}>
{noteNumber(n.id)}
</a>
))}{" "}
</span>
))}
</p>
<small className="text-[12px] text-muted">Built only from the notes. Each sentence links to its notes.</small>
</div>
);
}
/** The call text, the source of every note: kept 12 months as text, never as audio. */
export function CallText({ call, first, lit, noteNumber }: { call: CheckIn; first: string; lit: string | null; noteNumber: (id: string) => number }) {
return (
<section
aria-label="Call text"
className="flex min-w-0 flex-col gap-3 rounded-2xl bg-page p-5 shadow-card max-sm:p-4 @min-[1100px]/pd:sticky @min-[1100px]/pd:top-[72px] @min-[1100px]/pd:max-h-[calc(100dvh-96px)] @min-[1100px]/pd:overflow-y-auto"
>
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<h2 className="m-0 text-[15px] leading-5 font-semibold tracking-[-0.01em] text-ink">Call text</h2>
<span className="font-mono text-[11.5px] text-muted">Kept 12 months · no audio is stored</span>
</div>
{call.transcript.length ? (
<ol className="m-0 flex list-none flex-col gap-0.5 p-0">
{call.transcript.map((l) => {
const linked = call.notes.filter((n) => n.transcriptLineIds.includes(l.id));
return (
<li
key={l.id}
id={`line-${call.id}-${l.id}`}
className={cn("grid scroll-mt-24 grid-cols-[44px_minmax(0,1fr)] gap-x-2 rounded-lg px-2 py-1.5 transition-colors duration-300", lit === l.id && "bg-amber-fill")}
>
<span className="pt-px font-mono text-[11.5px] text-subtle tabular-nums">{callOffset(l.offsetSeconds)}</span>
<span className="flex min-w-0 flex-col">
<span className={cn("text-[12px] leading-[18px] font-medium", l.speaker === "yasmar" ? "text-action" : "text-purple-text")}>{l.speaker === "yasmar" ? "Yasmar" : first}</span>
<span className="text-[13.5px] leading-5 text-body">
{l.text}
{linked.map((n) => (
<a key={n.id} className={refClass} href={`#note-${n.id}`} aria-label={`Note ${noteNumber(n.id)}`}>
{noteNumber(n.id)}
</a>
))}
</span>
</span>
</li>
);
})}
</ol>
) : (
<p className="m-0 text-[13px] text-muted">The call text appears once {first} accepts.</p>
)}
</section>
);
}