/** * CallEventRow — enterprise-grade inline render of a phone call in * the chat thread. Replaces the heavy PostCallCard card on desktop. * * Industry pattern (Slack Huddle / MS Teams "Meeting ended" / * Google Chat video event): a call is a SYSTEM EVENT in the * timeline, not a content block. We render it as: * * State 1 — Default (collapsed): * * ────────── 📞 Call · 26s · 22:54 ────────── * * Thin centered divider, 12 px muted text, no box, no colored * button. The whole label is clickable to expand. * * State 2 — Hover (desktop): * * ── 📞 Call · 26s · 22:54 ── Transcript · ↻ Resume ── * * Action links fade in on hover (opacity 0 → 100). Text-weight, * not pill buttons. Dot-separated, reads as one phrase. * * State 3 — Expanded (on click): * * ── 📞 Call · 26s · 22:54 ── Collapse · ↻ Resume ── * You are you very sexy right * Secretary Sexy That's a rather direct question… * * Transcript prints inline unframed, indented, with a muted * speaker label column. No background, no border, no rounded * corners — just indented text that reads as part of the flow. * * Keeps PhoneCallDivider as the Voice-mode renderer (quieter, no * actions); CallEventRow is the chat-mode companion that adds the * reveal-on-demand affordances. Both components share the visual * language so the product reads consistently across surfaces. */ import React, { useState } from 'react'; import { Phone, RotateCcw } from 'lucide-react'; function formatDuration(s) { const clamped = Math.max(0, Math.floor(s)); if (clamped < 60) return `${clamped}s`; const m = Math.floor(clamped / 60); const rest = clamped % 60; return rest === 0 ? `${m}m` : `${m}m ${rest}s`; } function formatTime(ms) { try { return new Date(ms).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', }); } catch { return ''; } } /** Strip markdown image syntax from a transcript line so the raw * URL doesn't render as text. Mirrors the sanitizer applied to * TTS in App.tsx (stripMarkdownForSpeech). */ function stripTranscriptMarkup(text) { return text .replace(/!\[[^\]]*\]\([^)]+\)/g, '') // images .replace(/\s+/g, ' ') .trim(); } const CallEventRow = ({ durationSec, endedAt, personaName, transcript, onResume, defaultExpanded = false, className, }) => { const [expanded, setExpanded] = useState(defaultExpanded); const hasTranscript = !!(transcript && transcript.length > 0); const canResume = !!onResume; const hasActions = hasTranscript || canResume; const timeLabel = endedAt ? formatTime(endedAt) : ''; const durLabel = formatDuration(durationSec); const summary = timeLabel ? `Call · ${durLabel} · ${timeLabel}` : `Call · ${durLabel}`; const labelBtnLabel = expanded ? 'Collapse call row' : 'Expand call row'; const rowClickable = hasTranscript; return (
{/* Divider rule + centered label + hover action cluster */}
{hasActions && ()}
{/* Expanded transcript — inline, unframed */} {expanded && hasTranscript && (
{transcript.map((line, i) => { const label = line.who === 'user' ? 'You' : (personaName || 'Assistant'); return (
{label}
{stripTranscriptMarkup(line.text)}
); })}
)}
); }; export default CallEventRow;