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.
- package/dist/Timeline.cjs.js +1 -6
- package/dist/Timeline.cjs.js.map +1 -1
- package/dist/Timeline.es.js +129 -597
- package/dist/Timeline.es.js.map +1 -1
- package/dist/TranscriptCard.cjs.js +7 -0
- package/dist/TranscriptCard.cjs.js.map +1 -0
- package/dist/TranscriptCard.es.js +474 -0
- package/dist/TranscriptCard.es.js.map +1 -0
- package/dist/UpdatedInteractionRecording.cjs.js +1 -1
- package/dist/UpdatedInteractionRecording.cjs.js.map +1 -1
- package/dist/UpdatedInteractionRecording.es.js +516 -420
- package/dist/UpdatedInteractionRecording.es.js.map +1 -1
- package/dist/components/UpdatedInteractionDetails.cjs.js +2 -2
- package/dist/components/UpdatedInteractionDetails.cjs.js.map +1 -1
- package/dist/components/UpdatedInteractionDetails.es.js +304 -285
- package/dist/components/UpdatedInteractionDetails.es.js.map +1 -1
- package/dist/components/media.cjs.js +1 -1
- package/dist/components/media.cjs.js.map +1 -1
- package/dist/components/media.es.js +9 -8
- package/dist/components/media.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs2.js +1 -1
- package/dist/index.cjs2.js.map +1 -1
- package/dist/index.cjs3.js +2 -2
- package/dist/index.cjs3.js.map +1 -1
- package/dist/index.es.js +53 -52
- package/dist/index.es.js.map +1 -1
- package/dist/index.es2.js +1 -1
- package/dist/index.es2.js.map +1 -1
- package/dist/index.es3.js +11 -4
- package/dist/index.es3.js.map +1 -1
- package/dist/pages/interactionDetails.cjs.js +2 -2
- package/dist/pages/interactionDetails.cjs.js.map +1 -1
- package/dist/pages/interactionDetails.es.js +17 -16
- package/dist/pages/interactionDetails.es.js.map +1 -1
- package/package.json +1 -1
- package/src/components/UpdatedInteractionDetails/UpdatedInteractionDetails.jsx +125 -107
- 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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
69
|
-
const [demoPlaying, setDemoPlaying] = useState(
|
|
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
|
-
|
|
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
|
|
113
|
-
|
|
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 (
|
|
120
|
-
|
|
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 (
|
|
125
|
-
else
|
|
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 (
|
|
201
|
+
if (parentManaged && externalOnSeekForward) externalOnSeekForward();
|
|
130
202
|
else {
|
|
131
|
-
const
|
|
132
|
-
|
|
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 (
|
|
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 &&
|
|
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:
|
|
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
|
-
|
|
189
|
-
const effectiveTime =
|
|
190
|
-
const
|
|
191
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
{/*
|
|
211
|
-
|
|
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:
|
|
369
|
+
fontSize: 14, fontWeight: 500,
|
|
214
370
|
color: 'var(--Grey-Strong, #2E3236)',
|
|
215
371
|
lineHeight: 1.2,
|
|
216
372
|
}}>
|
|
217
|
-
|
|
373
|
+
{agentName} / {customerName}
|
|
218
374
|
</span>
|
|
219
375
|
|
|
220
|
-
{/*
|
|
221
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 12
|
|
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={
|
|
319
|
-
onSeek={
|
|
474
|
+
currentTimeSeconds={externalCurrentTime}
|
|
475
|
+
onSeek={externalOnSeek}
|
|
320
476
|
showControls={false}
|
|
321
477
|
hasRecording
|
|
322
|
-
timelinePlaying={
|
|
323
|
-
playbackRate={
|
|
478
|
+
timelinePlaying={externalPlaying}
|
|
479
|
+
playbackRate={externalRate}
|
|
324
480
|
/>
|
|
325
|
-
<audio ref={
|
|
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(
|
|
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
|
-
|
|
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
|
-
{['
|
|
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 *
|
|
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 === '
|
|
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 +
|
|
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(
|
|
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
|
-
{
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
{
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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:
|
|
722
|
+
fontSize: 13, fontWeight: 400,
|
|
551
723
|
lineHeight: 1.2,
|
|
552
|
-
color:
|
|
724
|
+
color: 'var(--Grey-Muted, #808183)',
|
|
553
725
|
}}>
|
|
554
|
-
{card.
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
604
|
-
|
|
764
|
+
</div>
|
|
765
|
+
);
|
|
766
|
+
})}
|
|
767
|
+
</div>
|
|
605
768
|
</div>
|
|
769
|
+
</div>{/* end card */}
|
|
606
770
|
</div>
|
|
607
771
|
);
|
|
608
772
|
});
|