chordia-ui 3.4.0 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/Timeline.cjs.js +1 -6
  2. package/dist/Timeline.cjs.js.map +1 -1
  3. package/dist/Timeline.es.js +129 -597
  4. package/dist/Timeline.es.js.map +1 -1
  5. package/dist/TranscriptCard.cjs.js +7 -0
  6. package/dist/TranscriptCard.cjs.js.map +1 -0
  7. package/dist/TranscriptCard.es.js +474 -0
  8. package/dist/TranscriptCard.es.js.map +1 -0
  9. package/dist/UpdatedInteractionRecording.cjs.js +1 -1
  10. package/dist/UpdatedInteractionRecording.cjs.js.map +1 -1
  11. package/dist/UpdatedInteractionRecording.es.js +516 -420
  12. package/dist/UpdatedInteractionRecording.es.js.map +1 -1
  13. package/dist/components/UpdatedInteractionDetails.cjs.js +2 -2
  14. package/dist/components/UpdatedInteractionDetails.cjs.js.map +1 -1
  15. package/dist/components/UpdatedInteractionDetails.es.js +304 -285
  16. package/dist/components/UpdatedInteractionDetails.es.js.map +1 -1
  17. package/dist/components/media.cjs.js +1 -1
  18. package/dist/components/media.cjs.js.map +1 -1
  19. package/dist/components/media.es.js +9 -8
  20. package/dist/components/media.es.js.map +1 -1
  21. package/dist/index.cjs.js +1 -1
  22. package/dist/index.cjs2.js +1 -1
  23. package/dist/index.cjs2.js.map +1 -1
  24. package/dist/index.cjs3.js +2 -2
  25. package/dist/index.cjs3.js.map +1 -1
  26. package/dist/index.es.js +53 -52
  27. package/dist/index.es.js.map +1 -1
  28. package/dist/index.es2.js +1 -1
  29. package/dist/index.es2.js.map +1 -1
  30. package/dist/index.es3.js +11 -4
  31. package/dist/index.es3.js.map +1 -1
  32. package/dist/pages/interactionDetails.cjs.js +2 -2
  33. package/dist/pages/interactionDetails.cjs.js.map +1 -1
  34. package/dist/pages/interactionDetails.es.js +17 -16
  35. package/dist/pages/interactionDetails.es.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/UpdatedInteractionDetails/UpdatedInteractionDetails.jsx +125 -107
  38. package/src/components/UpdatedInteractionDetails/UpdatedInteractionRecording.jsx +342 -178
@@ -1,11 +1,11 @@
1
- import { useRef, useState, forwardRef, useImperativeHandle } from 'react';
1
+ import { useRef, useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
2
2
  import {
3
3
  Play, Pause, RotateCcw, RotateCw,
4
4
  ChevronDown,
5
5
  AudioLines, PlayCircle, PauseCircle,
6
+ FileSignal,
6
7
  } from 'lucide-react';
7
8
  import Timeline from '../media/Timeline.jsx';
8
- import TranscriptCard from '../media/TranscriptCard.jsx';
9
9
 
10
10
  function fmtTime(seconds) {
11
11
  const m = Math.floor(seconds / 60);
@@ -15,7 +15,7 @@ function fmtTime(seconds) {
15
15
 
16
16
  /* ── Demo data for static preview ── */
17
17
  const DEMO_SEGMENTS = {
18
- David: [
18
+ Agent: [
19
19
  { start: 0, end: 0.08 },
20
20
  { start: 0.18, end: 0.22 },
21
21
  { start: 0.38, end: 0.42 },
@@ -34,27 +34,40 @@ const DEMO_SEGMENTS = {
34
34
  };
35
35
 
36
36
  const DEMO_TRANSCRIPT = [
37
- { speaker: 'David', type: 'agent', time: '00:04', text: 'Thanks for calling Miles Point S Pensau. This is Steve. How can I help you?' },
37
+ { speaker: 'Agent', type: 'agent', time: '00:04', text: 'Thanks for calling Miles Point S Pensau. This is Steve. How can I help you?' },
38
38
  { speaker: 'Customer', type: 'customer', time: '00:12', text: 'Hi, Steve. This is Sandra with Botai Catering.' },
39
- { speaker: 'David', type: 'agent', time: '00:18', text: 'Hello. How are you?' },
39
+ { speaker: 'Agent', type: 'agent', time: '00:18', text: 'Hello. How are you?' },
40
40
  { speaker: 'Customer', type: 'customer', time: '00:20', text: "I'm doing really good. Hey. I brought my vans in last week, and I call it a beeping van, the one that beeps when you back up. Makes the crunchy noise. So I just took it on Saturday after, I think I had the, the Miles. I picked it up on Saturday morning, and I went to an event. So So I don't know if it's the tire or something, but makes it a little crunchy noise. Sometimes when I do the brake, but only if I turn the wheel too." },
41
- { speaker: 'David', type: 'agent', time: '00:21', text: 'Oh, interesting. Okay. Can you swing by with it one of these days?' },
41
+ { speaker: 'Agent', type: 'agent', time: '00:21', text: 'Oh, interesting. Okay. Can you swing by with it one of these days?' },
42
42
  ];
43
43
 
44
+ const SPEED_OPTIONS = [1, 1.25, 1.5, 2];
45
+
46
+ function parseTimeStr(timeStr) {
47
+ if (!timeStr) return 0;
48
+ const parts = timeStr.split(':').map(Number);
49
+ return (parts[0] || 0) * 60 + (parts[1] || 0);
50
+ }
51
+
44
52
  /* ── Component ── */
45
53
  const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecording({
46
54
  audioUrl,
47
55
  timelineSegments = [],
48
56
  durationSeconds = 0,
49
- currentTimeSeconds = 0,
50
- timelinePlaying = false,
51
- playbackRate = 1,
52
- onSeek,
53
- onTogglePlay,
54
- onSeekBack,
55
- onSeekForward,
56
- onSetPlaybackRate,
57
- audioRef,
57
+ // Parent-managed audio props (optional — if not provided, component manages its own audio)
58
+ currentTimeSeconds: externalCurrentTime,
59
+ timelinePlaying: externalPlaying,
60
+ playbackRate: externalRate,
61
+ onSeek: externalOnSeek,
62
+ onTogglePlay: externalOnTogglePlay,
63
+ onSeekBack: externalOnSeekBack,
64
+ onSeekForward: externalOnSeekForward,
65
+ onSetPlaybackRate: externalOnSetPlaybackRate,
66
+ audioRef: externalAudioRef,
67
+ // Speaker names
68
+ agentName = 'Agent',
69
+ customerName = 'Customer',
70
+ // Transcript props
58
71
  transcript,
59
72
  highlightedTurns = new Set(),
60
73
  activeTurnIndex,
@@ -64,12 +77,69 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
64
77
  onTurnPlayPause,
65
78
  }, ref) {
66
79
  const scrollRef = useRef(null);
80
+ const internalAudioRef = useRef(null);
81
+ const animFrameRef = useRef(null);
82
+
83
+ // Internal audio state (used when audioUrl is provided but parent doesn't manage playback)
84
+ const [internalPlaying, setInternalPlaying] = useState(false);
85
+ const [internalCurrentTime, setInternalCurrentTime] = useState(0);
86
+ const [internalDuration, setInternalDuration] = useState(0);
87
+ const [internalRate, setInternalRate] = useState(1);
88
+
89
+ // Demo state (used when no audioUrl)
67
90
  const [demoSeekTime, setDemoSeekTime] = useState(null);
68
- const [demoRate, setDemoRate] = useState(playbackRate || 1);
69
- const [demoPlaying, setDemoPlaying] = useState(timelinePlaying);
91
+ const [demoRate, setDemoRate] = useState(1);
92
+ const [demoPlaying, setDemoPlaying] = useState(false);
70
93
  const [showSpeedMenu, setShowSpeedMenu] = useState(false);
71
94
  const [activeDemoIdx, setActiveDemoIdx] = useState(activeDemoIndex);
72
95
 
96
+ // Determine control mode
97
+ const parentManaged = !!externalAudioRef;
98
+ const isDemo = !audioUrl;
99
+ const selfManaged = audioUrl && !parentManaged;
100
+
101
+ // Unified state
102
+ const activePlaying = isDemo ? demoPlaying : (parentManaged ? (externalPlaying ?? false) : internalPlaying);
103
+ const activeCurrentTime = isDemo ? (demoSeekTime ?? 0) : (parentManaged ? (externalCurrentTime ?? 0) : internalCurrentTime);
104
+ const activeRate = isDemo ? demoRate : (parentManaged ? (externalRate ?? 1) : internalRate);
105
+ const activeDuration = isDemo ? (durationSeconds || 156) : (parentManaged ? durationSeconds : (internalDuration || durationSeconds || 0));
106
+ const audioRefToUse = parentManaged ? externalAudioRef : internalAudioRef;
107
+
108
+ /* ── Internal audio time tracking via requestAnimationFrame ── */
109
+ const startTimeTracking = useCallback(() => {
110
+ const tick = () => {
111
+ const audio = internalAudioRef.current;
112
+ if (audio) setInternalCurrentTime(audio.currentTime);
113
+ animFrameRef.current = requestAnimationFrame(tick);
114
+ };
115
+ animFrameRef.current = requestAnimationFrame(tick);
116
+ }, []);
117
+
118
+ const stopTimeTracking = useCallback(() => {
119
+ if (animFrameRef.current) {
120
+ cancelAnimationFrame(animFrameRef.current);
121
+ animFrameRef.current = null;
122
+ }
123
+ }, []);
124
+
125
+ // Cleanup on unmount
126
+ useEffect(() => () => stopTimeTracking(), [stopTimeTracking]);
127
+
128
+ // Wire up internal audio element events
129
+ useEffect(() => {
130
+ if (!selfManaged) return;
131
+ const audio = internalAudioRef.current;
132
+ if (!audio) return;
133
+ const onLoaded = () => setInternalDuration(audio.duration || 0);
134
+ const onEnded = () => { setInternalPlaying(false); stopTimeTracking(); };
135
+ audio.addEventListener('loadedmetadata', onLoaded);
136
+ audio.addEventListener('ended', onEnded);
137
+ return () => {
138
+ audio.removeEventListener('loadedmetadata', onLoaded);
139
+ audio.removeEventListener('ended', onEnded);
140
+ };
141
+ }, [selfManaged, stopTimeTracking]);
142
+
73
143
  /* Compute which transcript card matches current playback time */
74
144
  const computeActiveIdx = (timeSec) => {
75
145
  for (let i = DEMO_TRANSCRIPT.length - 1; i >= 0; i--) {
@@ -79,75 +149,126 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
79
149
  return 0;
80
150
  };
81
151
 
82
- const parseTimeStr = (timeStr) => {
83
- if (!timeStr) return 0;
84
- const parts = timeStr.split(':').map(Number);
85
- return (parts[0] || 0) * 60 + (parts[1] || 0);
86
- };
87
-
88
- const SPEED_OPTIONS = [1, 1.25, 1.5, 2];
89
- const isDemo = !audioUrl;
90
- const activeRate = isDemo ? demoRate : playbackRate;
91
- const activePlaying = isDemo ? demoPlaying : timelinePlaying;
92
-
93
- /* Expose seekTo for parent to call via ref */
94
- useImperativeHandle(ref, () => ({
95
- seekTo: (timeSec) => {
96
- handleSeek(timeSec);
97
- setDemoPlaying(true);
98
- // Scroll to matching transcript card
99
- const idx = computeActiveIdx(timeSec);
100
- setActiveDemoIdx(idx);
101
- setTimeout(() => {
102
- const cards = scrollRef.current?.children;
103
- if (cards?.[idx]) {
104
- cards[idx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
105
- }
106
- }, 50);
107
- },
108
- }));
109
-
110
- /* Handle seek — updates local demo state or calls parent onSeek */
152
+ /* ── Unified handlers ── */
111
153
  const handleSeek = (timeSeconds) => {
112
- const clamped = Math.max(0, Math.min(timeSeconds, durationSeconds || 156));
113
- if (onSeek) onSeek(clamped);
154
+ const dur = activeDuration || 156;
155
+ const clamped = Math.max(0, Math.min(timeSeconds, dur));
156
+
157
+ if (selfManaged) {
158
+ const audio = internalAudioRef.current;
159
+ if (audio) audio.currentTime = clamped;
160
+ setInternalCurrentTime(clamped);
161
+ } else if (parentManaged && externalOnSeek) {
162
+ externalOnSeek(clamped);
163
+ }
164
+
165
+ // Always update demo state for UI
114
166
  setDemoSeekTime(clamped);
115
167
  setActiveDemoIdx(computeActiveIdx(clamped));
116
168
  };
117
169
 
118
170
  const handleTogglePlay = () => {
119
- if (onTogglePlay) onTogglePlay();
120
- else setDemoPlaying((prev) => !prev);
171
+ if (selfManaged) {
172
+ const audio = internalAudioRef.current;
173
+ if (!audio) return;
174
+ if (internalPlaying) {
175
+ audio.pause();
176
+ stopTimeTracking();
177
+ setInternalPlaying(false);
178
+ } else {
179
+ audio.play().then(() => {
180
+ setInternalPlaying(true);
181
+ startTimeTracking();
182
+ }).catch(() => {});
183
+ }
184
+ } else if (parentManaged && externalOnTogglePlay) {
185
+ externalOnTogglePlay();
186
+ } else {
187
+ // Demo mode
188
+ setDemoPlaying((prev) => !prev);
189
+ }
121
190
  };
122
191
 
123
192
  const handleSeekBack = () => {
124
- if (onSeekBack) onSeekBack();
125
- else handleSeek(Math.max(0, (demoSeekTime ?? 0) - 10));
193
+ if (parentManaged && externalOnSeekBack) externalOnSeekBack();
194
+ else {
195
+ const cur = selfManaged ? internalCurrentTime : (demoSeekTime ?? 0);
196
+ handleSeek(Math.max(0, cur - 10));
197
+ }
126
198
  };
127
199
 
128
200
  const handleSeekForward = () => {
129
- if (onSeekForward) onSeekForward();
201
+ if (parentManaged && externalOnSeekForward) externalOnSeekForward();
130
202
  else {
131
- const demoDur = durationSeconds || 156;
132
- handleSeek(Math.min(demoDur, (demoSeekTime ?? 0) + 10));
203
+ const cur = selfManaged ? internalCurrentTime : (demoSeekTime ?? 0);
204
+ const dur = activeDuration || 156;
205
+ handleSeek(Math.min(dur, cur + 10));
133
206
  }
134
207
  };
135
208
 
136
209
  const handleSetRate = (rate) => {
137
- if (onSetPlaybackRate) onSetPlaybackRate(rate);
210
+ if (selfManaged) {
211
+ const audio = internalAudioRef.current;
212
+ if (audio) audio.playbackRate = rate;
213
+ setInternalRate(rate);
214
+ } else if (parentManaged && externalOnSetPlaybackRate) {
215
+ externalOnSetPlaybackRate(rate);
216
+ }
138
217
  setDemoRate(rate);
139
218
  setShowSpeedMenu(false);
140
219
  };
141
220
 
221
+ /* Expose seekTo for parent to call via ref (e.g. from Signals "Show in transcript") */
222
+ useImperativeHandle(ref, () => ({
223
+ seekTo: (timeSec) => {
224
+ handleSeek(timeSec);
225
+ if (selfManaged) {
226
+ const audio = internalAudioRef.current;
227
+ if (audio) {
228
+ audio.play().then(() => {
229
+ setInternalPlaying(true);
230
+ startTimeTracking();
231
+ }).catch(() => {});
232
+ }
233
+ } else {
234
+ setDemoPlaying(true);
235
+ }
236
+ const idx = computeActiveIdx(timeSec);
237
+ setActiveDemoIdx(idx);
238
+ setTimeout(() => {
239
+ const cards = scrollRef.current?.children;
240
+ if (cards?.[idx]) {
241
+ cards[idx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
242
+ }
243
+ }, 50);
244
+ },
245
+ }));
246
+
142
247
  /* Handle transcript card play/pause */
143
248
  const handleDemoTurnPlayPause = (idx) => {
144
- if (activeDemoIdx === idx && demoPlaying) {
249
+ if (activeDemoIdx === idx && activePlaying) {
250
+ if (selfManaged) {
251
+ const audio = internalAudioRef.current;
252
+ if (audio) audio.pause();
253
+ stopTimeTracking();
254
+ setInternalPlaying(false);
255
+ }
145
256
  setDemoPlaying(false);
146
257
  } else {
147
258
  const turnTime = parseTimeStr(DEMO_TRANSCRIPT[idx]?.time);
148
259
  setActiveDemoIdx(idx);
149
- setDemoPlaying(true);
150
260
  handleSeek(turnTime);
261
+ if (selfManaged) {
262
+ const audio = internalAudioRef.current;
263
+ if (audio) {
264
+ audio.currentTime = turnTime;
265
+ audio.play().then(() => {
266
+ setInternalPlaying(true);
267
+ startTimeTracking();
268
+ }).catch(() => {});
269
+ }
270
+ }
271
+ setDemoPlaying(true);
151
272
  }
152
273
  };
153
274
 
@@ -169,7 +290,7 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
169
290
  text: m.text || '',
170
291
  timeRange: formatTimeRange(m.start ?? m.start_ms, m.end ?? m.end_ms),
171
292
  isHighlighted: highlightedTurns.has(i),
172
- highlightColor: timelinePlaying && activeTurnIndex === i
293
+ highlightColor: activePlaying && activeTurnIndex === i
173
294
  ? (m.actor === 'agent' ? 'var(--rail-outcome)' : 'var(--rail-discovery)')
174
295
  : undefined,
175
296
  observations: (turnObservations[i] || []).map((obs) => ({
@@ -185,40 +306,75 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
185
306
  }))
186
307
  : null;
187
308
 
188
- const demoDuration = durationSeconds || 156;
189
- const effectiveTime = demoSeekTime != null ? demoSeekTime : (durationSeconds > 0 ? currentTimeSeconds : 0);
190
- const progress = demoDuration > 0 ? (effectiveTime / demoDuration) * 100 : 0;
191
- const demoCurrentTime = Math.round(effectiveTime);
309
+ // Computed display values
310
+ const effectiveTime = isDemo ? (demoSeekTime ?? 0) : activeCurrentTime;
311
+ const effectiveDuration = activeDuration || 156;
312
+ const progress = effectiveDuration > 0 ? (effectiveTime / effectiveDuration) * 100 : 0;
313
+ const displayTime = Math.round(effectiveTime);
192
314
 
193
315
  return (
194
316
  <div style={{
195
317
  display: 'flex',
196
- padding: 24,
197
318
  flexDirection: 'column',
198
- alignItems: 'flex-start',
199
- gap: 24,
200
- borderRadius: 8,
201
- border: '1px solid var(--Grey-absent, #D9D9D9)',
202
- background: 'var(--Grey-White, #FFF)',
319
+ gap: 16,
203
320
  }}>
321
+ {/* ── "Recording" title with icon — outside the card, matches Signals style ── */}
322
+ <div style={{
323
+ display: 'flex',
324
+ alignItems: 'center',
325
+ gap: 16,
326
+ width: '100%',
327
+ }}>
328
+ <div style={{
329
+ display: 'flex',
330
+ alignItems: 'center',
331
+ justifyContent: 'center',
332
+ width: 34,
333
+ height: 34,
334
+ borderRadius: 9999,
335
+ background: 'var(--surface-hover, #F3F7F7)',
336
+ flexShrink: 0,
337
+ }}>
338
+ <FileSignal size={20} color="#2E3236" strokeWidth={1.5} />
339
+ </div>
340
+ <span style={{
341
+ fontSize: 15,
342
+ fontWeight: 500,
343
+ color: 'var(--Grey-Strong, #2E3236)',
344
+ }}>
345
+ Recording
346
+ </span>
347
+ </div>
204
348
 
205
- {/* ════════════════════════════════════════
206
- RECORDING SECTION
207
- ════════════════════════════════════════ */}
208
- <div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
349
+ {/* ── Card with border ── */}
350
+ <div style={{
351
+ display: 'flex',
352
+ padding: 24,
353
+ flexDirection: 'column',
354
+ alignItems: 'flex-start',
355
+ gap: 24,
356
+ borderRadius: 8,
357
+ border: '1px solid var(--Grey-absent, #D9D9D9)',
358
+ background: 'var(--Grey-White, #FFF)',
359
+ }}>
209
360
 
210
- {/* ── Row 1: Title + Controls — Figma: Frame 35, horizontal, space-between, center ── */}
211
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 }}>
361
+ {/* ════════════════════════════════════════
362
+ RECORDING CONTROLS
363
+ ════════════════════════════════════════ */}
364
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
365
+
366
+ {/* ── "Agent / Customer" + Controls — horizontal, space-between, center ── */}
367
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 }}>
212
368
  <span style={{
213
- fontSize: 15, fontWeight: 600,
369
+ fontSize: 14, fontWeight: 500,
214
370
  color: 'var(--Grey-Strong, #2E3236)',
215
371
  lineHeight: 1.2,
216
372
  }}>
217
- Recording
373
+ {agentName} / {customerName}
218
374
  </span>
219
375
 
220
- {/* Frame 34: controls — horizontal, gap: 12, center */}
221
- <div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, justifyContent: 'center' }}>
376
+ {/* Controls — horizontal, gap: 12, center */}
377
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
222
378
  {/* Skip back 10s */}
223
379
  <span style={{ fontSize: 13, fontWeight: 400, color: 'var(--Grey-Muted, #808183)', lineHeight: 1.2 }}>10</span>
224
380
  <button onClick={handleSeekBack} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center' }}>
@@ -310,19 +466,19 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
310
466
  </div>
311
467
 
312
468
  {/* ── Row 2: Progress bar ── */}
313
- {audioUrl ? (
469
+ {audioUrl && parentManaged ? (
314
470
  <>
315
471
  <Timeline
316
472
  segments={timelineSegments}
317
473
  durationSeconds={durationSeconds}
318
- currentTimeSeconds={currentTimeSeconds}
319
- onSeek={onSeek}
474
+ currentTimeSeconds={externalCurrentTime}
475
+ onSeek={externalOnSeek}
320
476
  showControls={false}
321
477
  hasRecording
322
- timelinePlaying={timelinePlaying}
323
- playbackRate={playbackRate}
478
+ timelinePlaying={externalPlaying}
479
+ playbackRate={externalRate}
324
480
  />
325
- <audio ref={audioRef} preload="none" style={{ display: 'none' }}>
481
+ <audio ref={externalAudioRef} preload="none" style={{ display: 'none' }}>
326
482
  <source src={audioUrl} type="audio/mpeg" />
327
483
  </audio>
328
484
  </>
@@ -337,15 +493,14 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
337
493
  lineHeight: 1.2,
338
494
  minWidth: 60,
339
495
  }}>
340
- {fmtTime(demoCurrentTime)}
496
+ {fmtTime(displayTime)}
341
497
  </span>
342
498
  <div
343
499
  onClick={(e) => {
344
500
  const rect = e.currentTarget.getBoundingClientRect();
345
501
  const clickX = e.clientX - rect.left;
346
502
  const pct = Math.max(0, Math.min(1, clickX / rect.width));
347
- const seekTime = pct * demoDuration;
348
- handleSeek(seekTime);
503
+ handleSeek(pct * effectiveDuration);
349
504
  }}
350
505
  style={{
351
506
  flex: 1, height: 16,
@@ -394,7 +549,7 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
394
549
 
395
550
  {/* ── Row 3: Speaker timeline bars + playback indicator ── */}
396
551
  <div style={{ position: 'relative', paddingBottom: 30, cursor: 'pointer' }}>
397
- {['David', 'Customer'].map((speaker, rowIdx) => (
552
+ {['Agent', 'Customer'].map((speaker, rowIdx) => (
398
553
  <div key={speaker} style={{
399
554
  display: 'flex', alignItems: 'center', gap: 8,
400
555
  height: 20,
@@ -413,7 +568,7 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
413
568
  onClick={(e) => {
414
569
  const rect = e.currentTarget.getBoundingClientRect();
415
570
  const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
416
- handleSeek(pct * demoDuration);
571
+ handleSeek(pct * effectiveDuration);
417
572
  }}
418
573
  style={{
419
574
  flex: 1, height: 6,
@@ -435,7 +590,7 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
435
590
  left: `${seg.start * 100}%`,
436
591
  width: `${(seg.end - seg.start) * 100}%`,
437
592
  top: 0, bottom: 0, borderRadius: 3,
438
- background: speaker === 'David'
593
+ background: speaker === 'Agent'
439
594
  ? 'var(--Grey-Strong, #2E3236)'
440
595
  : 'var(--Grey-Muted, #808183)',
441
596
  }} />
@@ -444,7 +599,7 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
444
599
  </div>
445
600
  ))}
446
601
 
447
- {/* ── Playback position: dotted line + avatar + time tooltip ── */}
602
+ {/* ── Playback position: dotted line + time tooltip ── */}
448
603
  <div style={{
449
604
  position: 'absolute',
450
605
  left: 68, /* 60 label + 8 gap */
@@ -478,10 +633,17 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
478
633
  borderRadius: 4,
479
634
  whiteSpace: 'nowrap',
480
635
  }}>
481
- {fmtTime(demoCurrentTime)}
636
+ {fmtTime(displayTime)}
482
637
  </div>
483
638
  </div>
484
639
  </div>
640
+
641
+ {/* Hidden audio element for self-managed mode */}
642
+ {selfManaged && (
643
+ <audio ref={internalAudioRef} preload="auto" style={{ display: 'none' }}>
644
+ <source src={audioUrl} type="audio/mpeg" />
645
+ </audio>
646
+ )}
485
647
  </div>
486
648
  )}
487
649
  </div>
@@ -508,101 +670,103 @@ const UpdatedInteractionRecording = forwardRef(function UpdatedInteractionRecord
508
670
  </span>
509
671
  </div>
510
672
 
511
- {/* ── Cards ── */}
512
- {turns ? (
513
- <div id="transcript-container" ref={scrollRef} style={{ maxHeight: 600, overflowY: 'auto' }}>
514
- <TranscriptCard
515
- turns={turns}
516
- audioUrl={audioUrl}
517
- activeTurnIndex={activeTurnIndex}
518
- autoScrollActiveTurn={timelinePlaying}
519
- isExternalPlaying={timelinePlaying}
520
- onTurnPlayPause={onTurnPlayPause}
521
- />
522
- </div>
523
- ) : (
524
- <div ref={scrollRef} style={{
525
- display: 'flex', flexDirection: 'column',
526
- maxHeight: 600, overflowY: 'auto',
527
- }}>
528
- {DEMO_TRANSCRIPT.map((card, i) => {
529
- const isActive = i === activeDemoIdx && demoPlaying;
530
- const isCustomer = card.type === 'customer';
531
-
532
- return (
533
- <div key={i} style={{
534
- display: 'flex',
535
- padding: 16,
536
- flexDirection: 'column',
537
- alignItems: 'flex-start',
538
- gap: 16,
539
- alignSelf: 'stretch',
540
- borderTop: i > 0 ? '1px solid var(--Grey-absent, #D9D9D9)' : 'none',
541
- background: isActive ? 'var(--surface-hover, #F3F7F7)' : 'var(--Grey-White, #FFF)',
673
+ {/* ── Cards — V4 style for both real and demo data ── */}
674
+ <div id="transcript-container" ref={scrollRef} style={{
675
+ display: 'flex', flexDirection: 'column',
676
+ maxHeight: 600, overflowY: 'auto',
677
+ }}>
678
+ {(turns || DEMO_TRANSCRIPT.map((card) => ({
679
+ actor: card.speaker,
680
+ actorType: card.type,
681
+ text: card.text,
682
+ timeRange: card.time,
683
+ }))).map((card, i) => {
684
+ const isRealData = !!turns;
685
+ const isActive = isRealData
686
+ ? (activePlaying && activeTurnIndex === i)
687
+ : (i === activeDemoIdx && activePlaying);
688
+ const isCustomer = (card.actorType || card.type) === 'customer';
689
+
690
+ return (
691
+ <div key={i} style={{
692
+ display: 'flex',
693
+ padding: 16,
694
+ flexDirection: 'column',
695
+ alignItems: 'flex-start',
696
+ gap: 16,
697
+ alignSelf: 'stretch',
698
+ borderTop: i > 0 ? '1px solid var(--Grey-absent, #D9D9D9)' : 'none',
699
+ background: isActive ? 'var(--surface-hover, #F3F7F7)' : 'var(--Grey-White, #FFF)',
700
+ }}>
701
+ {/* Speaker name + time */}
702
+ <div style={{
703
+ display: 'flex', alignItems: 'center',
704
+ justifyContent: 'space-between', width: '100%',
705
+ gap: 8,
542
706
  }}>
543
- {/* Frame 40: Speaker name + time — horizontal, space-between, center */}
544
- <div style={{
545
- display: 'flex', alignItems: 'center',
546
- justifyContent: 'space-between', width: '100%',
547
- gap: 8,
707
+ <span style={{
708
+ fontSize: 14, fontWeight: 600,
709
+ lineHeight: 1.2,
710
+ color: isCustomer ? 'var(--Grey-Strong, #2E3236)' : 'var(--Grey-Muted, #808183)',
548
711
  }}>
712
+ {card.actor}
713
+ </span>
714
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
715
+ {isActive && (
716
+ <>
717
+ <AudioLines size={12} color="#2E3236" />
718
+ <AudioLines size={12} color="#2E3236" />
719
+ </>
720
+ )}
549
721
  <span style={{
550
- fontSize: 14, fontWeight: 600,
722
+ fontSize: 13, fontWeight: 400,
551
723
  lineHeight: 1.2,
552
- color: isCustomer ? 'var(--Grey-Strong, #2E3236)' : 'var(--Grey-Muted, #808183)',
724
+ color: 'var(--Grey-Muted, #808183)',
553
725
  }}>
554
- {card.speaker}
726
+ {card.timeRange}
555
727
  </span>
556
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
557
- {isActive && (
558
- <>
559
- <AudioLines size={12} color="#2E3236" />
560
- <AudioLines size={12} color="#2E3236" />
561
- </>
562
- )}
563
- <span style={{
564
- fontSize: 13, fontWeight: 400,
565
- lineHeight: 1.2,
566
- color: 'var(--Grey-Muted, #808183)',
567
- }}>
568
- {card.time}
569
- </span>
570
- </div>
571
728
  </div>
729
+ </div>
572
730
 
573
- {/* Frame 39: Play/Pause icon + text — horizontal, gap: 8 */}
574
- <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, width: '100%' }}>
575
- <button
576
- onClick={() => handleDemoTurnPlayPause(i)}
577
- style={{
578
- background: 'none', border: 'none',
579
- cursor: 'pointer', padding: 0,
580
- flexShrink: 0,
581
- display: 'flex',
582
- }}
583
- >
584
- {isActive ? (
585
- <PauseCircle size={17} color="#2E3236" strokeWidth={1.5} />
586
- ) : (
587
- <PlayCircle size={17} color="#808183" strokeWidth={1} />
588
- )}
589
- </button>
590
- <p style={{
591
- fontSize: 13, fontWeight: 400,
592
- color: 'var(--Grey-Strong, #2E3236)',
593
- lineHeight: 1.2,
594
- margin: 0,
595
- flex: 1,
596
- }}>
597
- {card.text}
598
- </p>
599
- </div>
731
+ {/* Play/Pause icon + text */}
732
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, width: '100%' }}>
733
+ <button
734
+ onClick={() => {
735
+ if (isRealData && onTurnPlayPause) {
736
+ onTurnPlayPause(card, i);
737
+ } else {
738
+ handleDemoTurnPlayPause(i);
739
+ }
740
+ }}
741
+ style={{
742
+ background: 'none', border: 'none',
743
+ cursor: 'pointer', padding: 0,
744
+ flexShrink: 0,
745
+ display: 'flex',
746
+ }}
747
+ >
748
+ {isActive ? (
749
+ <PauseCircle size={17} color="#2E3236" strokeWidth={1.5} />
750
+ ) : (
751
+ <PlayCircle size={17} color="#808183" strokeWidth={1} />
752
+ )}
753
+ </button>
754
+ <p style={{
755
+ fontSize: 13, fontWeight: 400,
756
+ color: 'var(--Grey-Strong, #2E3236)',
757
+ lineHeight: 1.2,
758
+ margin: 0,
759
+ flex: 1,
760
+ }}>
761
+ {card.text}
762
+ </p>
600
763
  </div>
601
- );
602
- })}
603
- </div>
604
- )}
764
+ </div>
765
+ );
766
+ })}
767
+ </div>
605
768
  </div>
769
+ </div>{/* end card */}
606
770
  </div>
607
771
  );
608
772
  });