File size: 6,113 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
 * 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 (<div role="region" aria-label={summary} className={['group my-2', className ?? ''].join(' ')}>
      {/* Divider rule + centered label + hover action cluster */}
      <div className="flex items-center gap-3 text-white/40 text-[12px] tracking-wide select-none">
        <div className="flex-1 h-px bg-white/10"/>

        <button type="button" onClick={() => { if (rowClickable)
        setExpanded((v) => !v); }} disabled={!rowClickable} aria-label={labelBtnLabel} className={[
            'flex items-center gap-1.5 whitespace-nowrap transition-colors',
            rowClickable
                ? 'cursor-pointer hover:text-white/70'
                : 'cursor-default',
            'bg-transparent border-0 p-0 m-0 font-inherit',
        ].join(' ')}>
          <Phone size={11} aria-hidden="true" className="opacity-70"/>
          <span>{summary}</span>
        </button>

        {hasActions && (<div className={[
                'flex items-center gap-2 text-[11px] text-white/45 whitespace-nowrap',
                // Hidden at rest, reveals on hover of the region.
                // Always visible when the row is expanded so the
                // Collapse link is reachable without re-hovering.
                expanded ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
                'transition-opacity duration-150',
            ].join(' ')}>
            {hasTranscript && (<button type="button" onClick={(e) => { e.stopPropagation(); setExpanded((v) => !v); }} className="bg-transparent border-0 p-0 m-0 cursor-pointer hover:text-white/85 transition-colors">
                {expanded ? 'Collapse' : 'Transcript'}
              </button>)}
            {hasTranscript && canResume && (<span className="text-white/20" aria-hidden="true">Β·</span>)}
            {canResume && (<button type="button" onClick={(e) => { e.stopPropagation(); onResume(); }} className="inline-flex items-center gap-1 bg-transparent border-0 p-0 m-0 cursor-pointer hover:text-white/85 transition-colors" aria-label="Resume call">
                <RotateCcw size={10} aria-hidden="true" className="opacity-80"/>
                <span>Resume</span>
              </button>)}
          </div>)}

        <div className="flex-1 h-px bg-white/10"/>
      </div>

      {/* Expanded transcript β€” inline, unframed */}
      {expanded && hasTranscript && (<div className="mt-3 mb-1 space-y-1.5">
          {transcript.map((line, i) => {
                const label = line.who === 'user' ? 'You' : (personaName || 'Assistant');
                return (<div key={i} className="flex gap-4 text-[13.5px] leading-relaxed">
                <div className="w-28 flex-shrink-0 text-white/35 text-[11px] uppercase tracking-wider pt-0.5">
                  {label}
                </div>
                <div className="flex-1 text-white/75">
                  {stripTranscriptMarkup(line.text)}
                </div>
              </div>);
            })}
        </div>)}
    </div>);
};
export default CallEventRow;