Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ const agentLoginInputError = document.getElementById('agent-login-input-error');
const applyupdateAgentProfileBtn = document.querySelector('#applyupdateAgentProfile');
const autoWrapupTimerElm = document.getElementById('autoWrapupTimer');
const timerValueElm = autoWrapupTimerElm.querySelector('.timer-value');
const fetchIvrTranscriptBtn = document.querySelector('#fetch-ivr-transcript');
const ivrTimeoutInput = document.querySelector('#timeout-mins');
const ivrStatusElm = document.querySelector('#ivr-transcript-status');
const ivrResultsContentElm = document.querySelector('#ivr-conversation-list');
console.log('IVR Transcript UI elements initialized:', {
fetchButton: !!fetchIvrTranscriptBtn,
timeoutInput: !!ivrTimeoutInput,
statusElement: !!ivrStatusElm,
resultsElement: !!ivrResultsContentElm
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check all the console logs and keep only relevant ones

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed irrelevant logs and kept only the necessary ones.

deregisterBtn.style.backgroundColor = 'red';

// Store and Grab `access-token` from sessionStorage
Expand Down Expand Up @@ -584,6 +594,142 @@ function refreshUIPostConsult() {
hideEndConsultButton();
}

// Function to update IVR transcript button state
function updateIvrTranscriptButtonState() {
console.log('Updating IVR transcript button state...');

if (!currentTask) {
console.log('IVR Button State: No active task found');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'No active task';
return;
}
console.log('IVR current task ', currentTask);
console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.mediaType}`);

if (currentTask.data.interaction.state === 'connected' && currentTask.data.mediaType === 'telephony') {
console.log('IVR Button State: Enabling button - task accepted and telephony channel');
fetchIvrTranscriptBtn.disabled = false;
ivrStatusElm.textContent = 'Ready to fetch IVR transcript';
} else if (currentTask.data.interaction.state !== 'connected') {
console.log('IVR Button State: Disabling button - task not accepted');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'Task must be accepted first';
} else if (currentTask.data.mediaType !== 'telephony') {
console.log('IVR Button State: Disabling button - not telephony channel');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'IVR transcript only available for telephony';
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: using currentTask.data.mediaType (should be currentTask.data.interaction.mediaType)

Task data stores media type under data.interaction.mediaType throughout the sample. Using data.mediaType will mis-detect channel and disable the button incorrectly.

Apply this diff:

-  console.log('IVR current task ', currentTask);
-  console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.mediaType}`);
+  console.log('IVR current task ', currentTask);
+  console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.interaction.mediaType}`);
 
-  if (currentTask.data.interaction.state === 'connected' && currentTask.data.mediaType === 'telephony') {
+  if (currentTask.data.interaction.state === 'connected' && currentTask.data.interaction.mediaType === 'telephony') {
     console.log('IVR Button State: Enabling button - task accepted and telephony channel');
     fetchIvrTranscriptBtn.disabled = false;
     ivrStatusElm.textContent = 'Ready to fetch IVR transcript';
   } else if (currentTask.data.interaction.state !== 'connected') {
     console.log('IVR Button State: Disabling button - task not accepted');
     fetchIvrTranscriptBtn.disabled = true;
     ivrStatusElm.textContent = 'Task must be accepted first';
-  } else if (currentTask.data.mediaType !== 'telephony') {
+  } else if (currentTask.data.interaction.mediaType !== 'telephony') {
     console.log('IVR Button State: Disabling button - not telephony channel');
     fetchIvrTranscriptBtn.disabled = true;
     ivrStatusElm.textContent = 'IVR transcript only available for telephony';
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Function to update IVR transcript button state
function updateIvrTranscriptButtonState() {
console.log('Updating IVR transcript button state...');
if (!currentTask) {
console.log('IVR Button State: No active task found');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'No active task';
return;
}
console.log('IVR current task ', currentTask);
console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.mediaType}`);
if (currentTask.data.interaction.state === 'connected' && currentTask.data.mediaType === 'telephony') {
console.log('IVR Button State: Enabling button - task accepted and telephony channel');
fetchIvrTranscriptBtn.disabled = false;
ivrStatusElm.textContent = 'Ready to fetch IVR transcript';
} else if (currentTask.data.interaction.state !== 'connected') {
console.log('IVR Button State: Disabling button - task not accepted');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'Task must be accepted first';
} else if (currentTask.data.mediaType !== 'telephony') {
console.log('IVR Button State: Disabling button - not telephony channel');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'IVR transcript only available for telephony';
}
}
// Function to update IVR transcript button state
function updateIvrTranscriptButtonState() {
console.log('Updating IVR transcript button state...');
if (!currentTask) {
console.log('IVR Button State: No active task found');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'No active task';
return;
}
console.log('IVR current task ', currentTask);
console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.interaction.mediaType}`);
if (currentTask.data.interaction.state === 'connected' && currentTask.data.interaction.mediaType === 'telephony') {
console.log('IVR Button State: Enabling button - task accepted and telephony channel');
fetchIvrTranscriptBtn.disabled = false;
ivrStatusElm.textContent = 'Ready to fetch IVR transcript';
} else if (currentTask.data.interaction.state !== 'connected') {
console.log('IVR Button State: Disabling button - task not accepted');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'Task must be accepted first';
} else if (currentTask.data.interaction.mediaType !== 'telephony') {
console.log('IVR Button State: Disabling button - not telephony channel');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'IVR transcript only available for telephony';
}
}
🤖 Prompt for AI Agents
In docs/samples/contact-center/app.js around lines 597 to 623, the code
incorrectly reads currentTask.data.mediaType (which is not where media type is
stored) causing wrong button state; change all uses to
currentTask.data.interaction.mediaType (including the console.log that reports
media channel and the conditional that checks for 'telephony') so the
media-channel detection and enabling/disabling logic use
currentTask.data.interaction.mediaType consistently.


// Function to fetch IVR transcript
async function fetchIvrTranscript() {
console.log('=== Starting IVR Transcript Fetch ===');

// Check if task is accepted and media type is telephony
if (!currentTask) {
console.error('IVR Fetch: No active task found');
ivrStatusElm.textContent = 'No active task found';
return;
}

console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.mediaType}`);

if (currentTask.data.interaction.state !== 'connected') {
console.warn('IVR Fetch: Task is not accepted');
ivrStatusElm.textContent = 'Task must be accepted to fetch IVR transcript';
return;
}

if (currentTask.data.mediaType !== 'telephony') {
console.warn('IVR Fetch: Media channel is not telephony');
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks';
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: mediaType path again

Same issue in fetchIvrTranscript preconditions; fix to reference interaction.mediaType.

-  console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.mediaType}`);
+  console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.interaction.mediaType}`);
@@
-  if (currentTask.data.mediaType !== 'telephony') {
+  if (currentTask.data.interaction.mediaType !== 'telephony') {
     console.warn('IVR Fetch: Media channel is not telephony');
     ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks';
     return;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Function to fetch IVR transcript
async function fetchIvrTranscript() {
console.log('=== Starting IVR Transcript Fetch ===');
// Check if task is accepted and media type is telephony
if (!currentTask) {
console.error('IVR Fetch: No active task found');
ivrStatusElm.textContent = 'No active task found';
return;
}
console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.mediaType}`);
if (currentTask.data.interaction.state !== 'connected') {
console.warn('IVR Fetch: Task is not accepted');
ivrStatusElm.textContent = 'Task must be accepted to fetch IVR transcript';
return;
}
if (currentTask.data.mediaType !== 'telephony') {
console.warn('IVR Fetch: Media channel is not telephony');
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks';
return;
// Function to fetch IVR transcript
async function fetchIvrTranscript() {
console.log('=== Starting IVR Transcript Fetch ===');
// Check if task is accepted and media type is telephony
if (!currentTask) {
console.error('IVR Fetch: No active task found');
ivrStatusElm.textContent = 'No active task found';
return;
}
console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.interaction.mediaType}`);
if (currentTask.data.interaction.state !== 'connected') {
console.warn('IVR Fetch: Task is not accepted');
ivrStatusElm.textContent = 'Task must be accepted to fetch IVR transcript';
return;
}
if (currentTask.data.interaction.mediaType !== 'telephony') {
console.warn('IVR Fetch: Media channel is not telephony');
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks';
return;
}
// …rest of function…
}
🤖 Prompt for AI Agents
In docs/samples/contact-center/app.js around lines 625 to 647, the precondition
checks for media type are incorrectly reading currentTask.data.mediaType instead
of the mediaType nested under interaction; update the code to reference
currentTask.data.interaction.mediaType everywhere in this function (including
the console.log and the if-check that validates 'telephony') so the
media-channel checks use interaction.mediaType consistently and
ivrStatus/messages remain accurate.

}

// Get timeout value from input (convert minutes to milliseconds)
const timeoutMinutes = parseInt(ivrTimeoutInput.value) || 5;
const timeoutMs = timeoutMinutes * 60 * 1000;

console.log(`IVR Fetch: Using timeout of ${timeoutMinutes} minutes (${timeoutMs}ms)`);

try {
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = `Fetching IVR transcript... (timeout: ${timeoutMinutes} min)`;
ivrResultsContentElm.innerHTML = '';

console.log('IVR Fetch: Calling currentTask.fetchIvrTranscript()...');
const transcript = await currentTask.fetchIvrTranscript(currentTask.data.orgId, currentTask.data.interactionId, timeoutMinutes);

console.log('IVR Fetch: Received response:', transcript);

if (transcript && transcript.length > 0) {
console.log(`IVR Fetch: Successfully fetched ${transcript.length} conversation(s)`);
ivrStatusElm.textContent = `Successfully fetched ${transcript.length} conversation(s)`;

// Show the content area
const ivrContentArea = document.querySelector('#ivr-transcript-content');
if (ivrContentArea) {
ivrContentArea.style.display = 'block';
}

// Display the conversations
let html = '';
transcript.forEach((conversation, index) => {
console.log(`IVR Fetch: Processing conversation ${index + 1}:`, conversation);
html += `
<div style="margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; border-radius: 5px;">
<h4>Conversation ${index + 1}</h4>
<p><strong>Call ID:</strong> ${conversation.callId || 'N/A'}</p>
<p><strong>Duration:</strong> ${conversation.duration || 'N/A'}</p>
<p><strong>Start Time:</strong> ${conversation.startTime || 'N/A'}</p>
<p><strong>End Time:</strong> ${conversation.endTime || 'N/A'}</p>
${conversation.segments && conversation.segments.length > 0 ? `
<div style="margin-top: 10px;">
<strong>Segments:</strong>
<ul style="margin-top: 5px;">
${conversation.segments.map(segment => `
<li style="margin-bottom: 5px;">
<strong>${segment.speaker || 'Unknown'}:</strong> ${segment.text || 'N/A'}
<br><small>Duration: ${segment.duration || 'N/A'}, Offset: ${segment.offset || 'N/A'}</small>
</li>
`).join('')}
</ul>
</div>
` : '<p><em>No segments available</em></p>'}
</div>
`;
});

ivrResultsContentElm.innerHTML = html;
console.log('IVR Fetch: UI updated with conversation data');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

XSS risk: unescaped user text inserted via innerHTML

Transcript fields (e.g., customer query) originate from user speech-to-text and must be treated as untrusted. Direct innerHTML interpolation is vulnerable to XSS. Escape values or assign via textContent/DOM nodes.

Apply minimal escaping in this block:

-            <p><strong>Call ID:</strong> ${conversation.callId || 'N/A'}</p>
-            <p><strong>Duration:</strong> ${conversation.duration || 'N/A'}</p>
-            <p><strong>Start Time:</strong> ${conversation.startTime || 'N/A'}</p>
-            <p><strong>End Time:</strong> ${conversation.endTime || 'N/A'}</p>
+            <p><strong>Call ID:</strong> ${escapeHtml(conversation.callId || 'N/A')}</p>
+            <p><strong>Duration:</strong> ${escapeHtml(conversation.duration || 'N/A')}</p>
+            <p><strong>Start Time:</strong> ${escapeHtml(conversation.startTime || 'N/A')}</p>
+            <p><strong>End Time:</strong> ${escapeHtml(conversation.endTime || 'N/A')}</p>
@@
-                  ${conversation.segments.map(segment => `
+                  ${conversation.segments.map(segment => `
                     <li style="margin-bottom: 5px;">
-                      <strong>${segment.speaker || 'Unknown'}:</strong> ${segment.text || 'N/A'}
-                      <br><small>Duration: ${segment.duration || 'N/A'}, Offset: ${segment.offset || 'N/A'}</small>
+                      <strong>${escapeHtml(segment.speaker || 'Unknown')}:</strong> ${escapeHtml(segment.text || 'N/A')}
+                      <br><small>Duration: ${escapeHtml(segment.duration || 'N/A')}, Offset: ${escapeHtml(segment.offset || 'N/A')}</small>
                     </li>
                   `).join('')}

Place this helper near the top of the file (or before usage):

// Escapes HTML special characters to prevent XSS in sample UI
function escapeHtml(value) {
  return String(value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

Note: The service returns IvrConversations as an array of turns ({customer?}/{bot?}). Consider rendering those turns directly in the UI instead of assuming conversation.segments/callId/duration, which may be undefined.

🤖 Prompt for AI Agents
In docs/samples/contact-center/app.js around lines 676-705, the code injects
transcript data directly into innerHTML leading to XSS risk; add an escapeHtml
helper near the top of the file (before usage) that replaces &, <, >, ", ' with
their HTML entities, then update this block to avoid raw innerHTML interpolation
by either building DOM nodes and setting textContent for all user-derived fields
(callId, duration, startTime, endTime, segment.speaker, segment.text, etc.) or,
if continuing to build a string, pass every user-derived value through
escapeHtml before inserting it; also handle missing fields safely and prefer
rendering turns (customer/bot) if available instead of assuming
segments/callId/duration exist.

} else {
console.log('IVR Fetch: No transcript data received');
ivrStatusElm.textContent = 'No IVR transcript found for this task';

// Hide the content area
const ivrContentArea = document.querySelector('#ivr-transcript-content');
if (ivrContentArea) {
ivrContentArea.style.display = 'none';
}

ivrResultsContentElm.innerHTML = '<p><em>No transcript data available</em></p>';
}
} catch (error) {
console.error('IVR Fetch: Error occurred:', error);
console.error('IVR Fetch: Error details:', {
message: error.message,
stack: error.stack,
name: error.name
});
ivrStatusElm.textContent = `Error: ${error.message || 'Failed to fetch IVR transcript'}`;
ivrResultsContentElm.innerHTML = '<p><em>Error occurred while fetching transcript</em></p>';
} finally {
fetchIvrTranscriptBtn.disabled = false;
console.log('=== IVR Transcript Fetch Complete ===');
}
}

// Register task listeners
function registerTaskListeners(task) {
task.on('task:assigned', (task) => {
Expand All @@ -595,6 +741,7 @@ function registerTaskListeners(task) {
document.getElementById('remote-audio').srcObject = new MediaStream([track]);
});
task.on('task:end', (task) => {
console.log('Task ended event received');
incomingDetailsElm.innerText = '';
if (currentTask.data.interactionId === task.data.interactionId) {
if (!task.data.wrapUpRequired) {
Expand All @@ -608,6 +755,8 @@ function registerTaskListeners(task) {
}
updateTaskList(); // Update the task list UI to have latest tasks
handleTaskSelect(task);
console.log('Updating IVR button state after task end...');
updateIvrTranscriptButtonState(); // Update IVR button state when task ends
}
});

Expand Down Expand Up @@ -1090,6 +1239,14 @@ function doDeRegister() {

deregisterBtn.addEventListener('click', doDeRegister);

// Add event listener for IVR transcript fetch button
console.log('Setting up IVR transcript fetch button event listener...');
fetchIvrTranscriptBtn.addEventListener('click', fetchIvrTranscript);

// Initialize IVR transcript button state
console.log('Initializing IVR transcript button state...');
updateIvrTranscriptButtonState();

function handleTaskHydrate(task) {
currentTask = task;

Expand Down Expand Up @@ -1318,12 +1475,15 @@ incomingCallListener.addEventListener('task:incoming', (event) => {
});

async function answer() {
console.log('Answer button clicked - accepting task...');
answerElm.disabled = true;
declineElm.disabled = true;
await currentTask.accept();
updateTaskList();
handleTaskSelect(currentTask);
incomingDetailsElm.innerText = 'Task Accepted';
console.log('Task accepted successfully, updating IVR button state...');
updateIvrTranscriptButtonState(); // Enable IVR transcript button if telephony
}

function decline() {
Expand Down Expand Up @@ -1682,17 +1842,21 @@ function disableAnswerDeclineButtons() {
function handleTaskSelect(task) {
// Handle the task click event
console.log('Task clicked:', task);
console.log(`Task details - ID: ${task.data?.interactionId}, Media: ${task.data?.interaction?.mediaType}`);
enableAnswerDeclineButtons(task);
engageElm.innerHTML = ``;
engageElm.style.height = "100px"
const chatAndSocial = ['chat', 'social'];
currentTask = task
console.log('Updating IVR button state for selected task...');
updateIvrTranscriptButtonState(); // Update IVR button state when task is selected
if (chatAndSocial.includes(task.data.interaction.mediaType) && isBundleLoaded && !task.data.wrapUpRequired) {
loadChatWidget(task);
} else if (task.data.interaction.mediaType === 'email' && isBundleLoaded && !task.data.wrapUpRequired) {
loadEmailWidget(task);
}
updateCallControlUI(task); // Enable/disable transfer controls
updateIvrTranscriptButtonState(); // Update IVR button state when task is selected
}

function loadChatWidget(task) {
Expand Down
14 changes: 14 additions & 0 deletions docs/samples/contact-center/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ <h2 class="collapsible">
<legend>TaskList</legend>
<div id="taskList"></div>
</fieldset>
<fieldset>
<legend>IVR Transcript</legend>
<div class="u-mv">
<button id="fetch-ivr-transcript" class="btn--blue" disabled>Fetch IVR Transcript</button>
<input id="timeout-mins" placeholder="Timeout (mins)" value="5" type="number" min="1" max="60" style="width: 120px; margin-left: 10px;">
</div>
<div id="ivr-transcript-result" style="margin-top: 10px;">
<p id="ivr-transcript-status" class="status-par">No transcript fetched</p>
<div id="ivr-transcript-content" style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-top: 10px; display: none;">
<h4>IVR Conversation:</h4>
<div id="ivr-conversation-list"></div>
</div>
</div>
</fieldset>
<fieldset>
<legend>Digital Channels</legend>
<div id="engageWidget"> </div>
Expand Down
7 changes: 7 additions & 0 deletions packages/@webex/plugin-cc/src/metrics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type Enum<T extends Record<string, unknown>> = T[keyof T];
* @property {string} TASK_ACCEPT_CONSULT_SUCCESS - Event name for successful consult acceptance.
* @property {string} TASK_ACCEPT_CONSULT_FAILED - Event name for failed consult acceptance.
*
* @property {string} TASK_IVR_TRANSCRIPT_FETCH_SUCCESS - Event name for successful IVR transcript fetch.
* @property {string} TASK_IVR_TRANSCRIPT_FETCH_FAILED - Event name for failed IVR transcript fetch.
*
* @property {string} TASK_OUTDIAL_SUCCESS - Event name for successful outdial task.
* @property {string} TASK_OUTDIAL_FAILED - Event name for failed outdial task.
*
Expand Down Expand Up @@ -109,6 +112,10 @@ export const METRIC_EVENT_NAMES = {
TASK_ACCEPT_CONSULT_SUCCESS: 'Task Accept Consult Success',
TASK_ACCEPT_CONSULT_FAILED: 'Task Accept Consult Failed',

// IVR Transcript
TASK_IVR_TRANSCRIPT_FETCH_SUCCESS: 'Task IVR Transcript Fetch Success',
TASK_IVR_TRANSCRIPT_FETCH_FAILED: 'Task IVR Transcript Fetch Failed',

TASK_OUTDIAL_SUCCESS: 'Task Outdial Success',
TASK_OUTDIAL_FAILED: 'Task Outdial Failed',

Expand Down
Loading
Loading