| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import React, { useState } from 'react'; |
| import { IconPhone, IconPhoneMissed, IconSparkle, } from './icons'; |
| import { CALL, POST_CALL } from './tokens'; |
| |
| 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', |
| }); |
| } |
| |
| 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); |
| |
| 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 (<div role="note" aria-label={missed ? `Missed call from ${personaName}` : `Call with ${personaName}`} className="hp-fade-in" style={{ |
| width: POST_CALL.cardWidth, |
| maxWidth: '100%', |
| borderRadius: POST_CALL.cardRadius, |
| overflow: 'hidden', |
| background: missed |
| ? `linear-gradient(180deg, color-mix(in oklch, ${accent} 12%, transparent) 0%, rgba(245,236,255,0.03) 100%)` |
| : 'linear-gradient(180deg, rgba(245,236,255,0.06) 0%, rgba(245,236,255,0.03) 100%)', |
| border: missed |
| ? `0.5px solid color-mix(in oklch, ${accent} 35%, transparent)` |
| : `0.5px solid ${CALL.line}`, |
| fontFamily: CALL.font, |
| color: CALL.ink, |
| }}> |
| {/* Header row — icon + title + time */} |
| <div style={{ |
| padding: POST_CALL.rowPadding, |
| display: 'flex', |
| alignItems: 'center', |
| gap: 10, |
| }}> |
| <div aria-hidden="true" style={{ |
| width: POST_CALL.avatarSize, |
| height: POST_CALL.avatarSize, |
| borderRadius: '50%', |
| background: `color-mix(in oklch, ${accent} 20%, transparent)`, |
| border: `0.5px solid color-mix(in oklch, ${accent} 40%, transparent)`, |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center', |
| color: accent, |
| flexShrink: 0, |
| }}> |
| {missed ? (<IconPhoneMissed size={14} color={accent}/>) : (<IconPhone size={14} color={accent}/>)} |
| </div> |
| <div style={{ flex: 1, minWidth: 0 }}> |
| <div style={{ |
| fontSize: 13, |
| fontWeight: 600, |
| letterSpacing: -0.1, |
| color: missed ? accent : CALL.ink, |
| whiteSpace: 'nowrap', |
| overflow: 'hidden', |
| textOverflow: 'ellipsis', |
| }}> |
| {titleText} |
| </div> |
| <div style={{ |
| fontSize: 11, |
| color: CALL.dim, |
| fontVariantNumeric: 'tabular-nums', |
| }}> |
| {subtitle} |
| </div> |
| </div> |
| </div> |
| |
| {/* Highlights body — italic summary line (only when variant is |
| 'highlights' and we're not in the missed state). */} |
| {variant === 'highlights' && !missed && summary ? (<div style={{ |
| padding: '4px 14px 12px', |
| fontFamily: CALL.display, |
| fontSize: 15, |
| lineHeight: 1.4, |
| color: 'rgba(245,236,255,0.82)', |
| fontStyle: 'italic', |
| }}> |
| <div style={{ |
| display: 'flex', |
| alignItems: 'center', |
| gap: 5, |
| marginBottom: 4, |
| fontFamily: CALL.font, |
| fontStyle: 'normal', |
| fontSize: 9, |
| color: 'oklch(0.80 0.15 340)', |
| letterSpacing: 1.4, |
| textTransform: 'uppercase', |
| }}> |
| <IconSparkle size={9} color="oklch(0.80 0.15 340)"/> |
| {personaName} remembers |
| </div> |
| <span>“{summary}”</span> |
| </div>) : 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 ? (<div style={{ |
| padding: '2px 14px 12px', |
| borderTop: `0.5px solid ${CALL.line}`, |
| display: 'flex', |
| flexDirection: 'column', |
| gap: 8, |
| }}> |
| <div style={{ |
| fontSize: 9, |
| letterSpacing: 1.4, |
| textTransform: 'uppercase', |
| color: 'rgba(245,236,255,0.4)', |
| paddingTop: 10, |
| }}> |
| Transcript |
| </div> |
| {transcript.map((line, i) => (<div key={i} style={{ fontSize: 12, lineHeight: 1.45 }}> |
| <span style={{ |
| color: line.who === 'user' |
| ? 'oklch(0.80 0.15 50)' |
| : 'oklch(0.80 0.15 340)', |
| fontWeight: 600, |
| marginRight: 6, |
| }}> |
| {line.who === 'user' ? 'You' : personaName} |
| </span> |
| <span style={{ color: 'rgba(245,236,255,0.85)' }}> |
| {line.text} |
| </span> |
| </div>))} |
| </div>) : null} |
| |
| {/* CTA row */} |
| <div style={{ |
| display: 'flex', |
| gap: 6, |
| padding: '8px 10px 10px', |
| borderTop: variant === 'expand' && expanded && !missed && hasTranscript |
| ? 'none' |
| : `0.5px solid ${CALL.line}`, |
| }}> |
| <button type="button" onClick={missed ? (onCallBack ?? onResume) : onResume} disabled={!(missed ? (onCallBack ?? onResume) : onResume)} aria-label={missed ? 'Call back' : 'Resume call'} style={{ |
| flex: 1, |
| height: POST_CALL.ctaHeight, |
| borderRadius: POST_CALL.ctaRadius, |
| cursor: 'pointer', |
| background: accent, |
| color: '#0c0810', |
| border: 'none', |
| fontFamily: CALL.font, |
| fontSize: 12, |
| fontWeight: 600, |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center', |
| gap: 6, |
| }}> |
| <IconPhone size={12} color="#0c0810"/> |
| {missed ? 'Call back' : 'Resume call'} |
| </button> |
| |
| {/* Secondary CTA — only the non-missed variants have one */} |
| {!missed && variant === 'highlights' && onOpenFullTranscript ? (<button type="button" onClick={onOpenFullTranscript} aria-label="Open full transcript" style={{ |
| flex: 1, |
| height: POST_CALL.ctaHeight, |
| borderRadius: POST_CALL.ctaRadius, |
| cursor: 'pointer', |
| background: 'rgba(245,236,255,0.06)', |
| color: CALL.ink, |
| border: `0.5px solid ${CALL.line}`, |
| fontFamily: CALL.font, |
| fontSize: 12, |
| fontWeight: 500, |
| }}> |
| Full transcript |
| </button>) : null} |
| |
| {!missed && variant === 'expand' && hasTranscript ? (<button type="button" onClick={() => setExpanded((v) => !v)} aria-expanded={expanded} aria-label={expanded ? 'Collapse transcript' : 'View transcript'} style={{ |
| flex: 1, |
| height: POST_CALL.ctaHeight, |
| borderRadius: POST_CALL.ctaRadius, |
| cursor: 'pointer', |
| background: 'rgba(245,236,255,0.06)', |
| color: CALL.ink, |
| border: `0.5px solid ${CALL.line}`, |
| fontFamily: CALL.font, |
| fontSize: 12, |
| fontWeight: 500, |
| }}> |
| {expanded ? 'Collapse ▴' : 'View transcript ▾'} |
| </button>) : null} |
| </div> |
| </div>); |
| }; |
| export default PostCallCard; |
| |
| export const postCallCardInternals = { |
| formatDuration, |
| formatClock, |
| }; |
|
|