/** * 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' // ── Props ──────────────────────────────────────────────────────── export type PostCallVariant = 'expand' | 'highlights' export interface TranscriptLine { who: 'user' | 'assistant' text: string } export interface PostCallCardProps { /** Live-phase duration in seconds. Rendered as "12s" / "1m 4s" / * "12 min" depending on magnitude. */ durationSec: number /** Epoch-ms timestamp of when the call ended. Rendered as * "8:14 PM" via toLocaleTimeString. */ endedAt?: number /** The persona the user was talking to. Drives the body text * ("Call with Vesper"). Default "Assistant". */ personaName?: string /** Variant — see module docstring. */ variant?: PostCallVariant /** Missed-call variant — tints red, shows phone-slash icon, * swaps the CTAs. */ missed?: boolean /** Optional transcript. When present AND variant='expand', * enables the inline expand button. */ transcript?: TranscriptLine[] /** Optional short summary for variant='highlights' * (e.g. "You talked about Lisbon and made plans for Friday"). */ summary?: string /** "Resume call" click — re-open the call overlay with * skipDialing=true. */ onResume?: () => void /** "Call back" click on the missed variant. Defaults to the same * as onResume when omitted. */ onCallBack?: () => void /** "Full transcript" click on the highlights variant — typically * opens a separate route or sheet. Optional; button hides when * handler isn't provided. */ onOpenFullTranscript?: () => void } // ── Helpers ────────────────────────────────────────────────────── function formatDuration(s: number): string { 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: number): string { return new Date(endedAt).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', }) } // ── Component ──────────────────────────────────────────────────── const PostCallCard: React.FC = ({ 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 as TranscriptLine[]).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, }