/** * PostCallCard — inline "we just talked" record rendered directly * in the chat thread, NOT as a modal. Product direction from the * engineering handoff: * * "You are building a conversational / companion experience, * not a compliance tool. → Use inline card + expandable * transcript." * * Three variants covering the full (variant × missed) matrix from * the handoff's § 3: * * expand — default. Duration + time + [Resume call] + * [View transcript ▾]. Tapping the latter expands * the transcript INLINE inside the same card (no * modal). Keeps the call record as an event in * the conversation the user can glance at later. * highlights — same shell, but the body is a short italic * "Vesper remembers…" summary + a [Full transcript] * link. For sessions where the call was long and * the full transcript is too long to inline. * missed — red tint, phone-slash icon, single [Call back] * CTA. Set via ``missed`` prop (orthogonal to * variant; a missed call is always rendered as the * missed variant, others ignored). * * This component replaces the prior enterprise-neutral * CallMemoryCard. The enterprise form (flat surface, \"Voice session * completed\" copy) is preserved as a setting-free default by * passing only durationSec + endedAt; richer props turn on the * conversational surface. */ import React, { useState } from 'react'; import { IconPhone, IconPhoneMissed, IconSparkle, } from './icons'; import { CALL, POST_CALL } from './tokens'; // ── Helpers ────────────────────────────────────────────────────── function formatDuration(s) { if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const r = s % 60; if (r === 0) return m < 60 ? `${m} min` : `${Math.floor(m / 60)}h ${m % 60}m`; return `${m} min ${r}s`; } function formatClock(endedAt) { return new Date(endedAt).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', }); } // ── Component ──────────────────────────────────────────────────── const PostCallCard = ({ durationSec, endedAt, personaName = 'Assistant', variant = 'expand', missed = false, transcript, summary, onResume, onCallBack, onOpenFullTranscript, }) => { const [expanded, setExpanded] = useState(false); const clock = endedAt ? formatClock(endedAt) : ''; const dur = formatDuration(durationSec); const accent = missed ? CALL.danger : CALL.rose; const hasTranscript = !!(transcript && transcript.length > 0); // Top row — icon, title, sub. const subtitle = missed ? clock ? `${clock} · tap to call back` : 'tap to call back' : clock ? `${dur} · ${clock}` : dur; const titleText = missed ? `Missed call · ${personaName}` : `Call with ${personaName}`; return (
{/* Header row — icon + title + time */}
{titleText}
{subtitle}
{/* Highlights body — italic summary line (only when variant is 'highlights' and we're not in the missed state). */} {variant === 'highlights' && !missed && summary ? (
{personaName} remembers
“{summary}”
) : null} {/* Expanded transcript — inline, NOT a modal. Renders only for variant='expand' when the user has clicked the expand button and a transcript was provided. */} {variant === 'expand' && expanded && !missed && hasTranscript ? (
Transcript
{transcript.map((line, i) => (
{line.who === 'user' ? 'You' : personaName} {line.text}
))}
) : null} {/* CTA row */}
{/* Secondary CTA — only the non-missed variants have one */} {!missed && variant === 'highlights' && onOpenFullTranscript ? () : null} {!missed && variant === 'expand' && hasTranscript ? () : null}
); }; export default PostCallCard; // Test hooks — exposed for unit tests without widening the public API. export const postCallCardInternals = { formatDuration, formatClock, };