This guide is intended for advanced Natterbox admins. Further customisations of this solution might not be supported.
Notice:
This is not a native Natterbox feature and requires a comprehensive knowledge of salesforce apex classes, visual studio code, and general lightning web component skills. We have provided a proof of concept guide for your review. Implementing and maintaining this solution falls outside the scope of support Natterbox can offer.
The AI Scorecard is a custom Lightning Web Component (LWC) that displays AI Advisor prompt results on VoiceCall or Task (wrap-up) record pages in an interactive, visual format. It presents numeric rating prompts as a polar area chart (the "Call Ratings" view) and free-text prompts as expandable accordion sections — giving agents and managers an at-a-glance view of call performance directly within Salesforce.
ℹ️
Note: This component reads AI Advisor results from the managed
nbavs__NatterboxAI__c(Natterbox AI) object. You no longer need to create a custom object or Salesforce Flows to populate results — all prompt data is stored automatically on this unified object. See Natterbox AI Object Fields & Definitions for the full schema reference.
What the AI Scorecard displays
The component provides two tabbed views on a Task or VoiceCall record page:
Call Ratings (Employee View) — A polar area chart visualising numeric AI rating prompts (e.g. Agent Performance, Customer Experience, Discovery & Needs Analysis). Hovering or clicking a chart segment reveals the score and detailed AI reasoning below the chart.
.png?sv=2026-02-06&spr=https&st=2026-06-21T18%3A37%3A05Z&se=2026-06-21T19%3A34%3A05Z&sr=c&sp=r&sig=6TOIqiwDeLTbM1Y7Xu6%2F2%2F6ABUNqp7nPN7Lov1NhBSo%3D)
Free-Text (Employee View) — An accordion list of all free-text AI prompt results for the call (e.g. Auto Wrap-Up, Summarise Call for Case, Transcript, Next Best Action). Click any row to expand and read the full AI-generated response.
.png?sv=2026-02-06&spr=https&st=2026-06-21T18%3A37%3A05Z&se=2026-06-21T19%3A34%3A05Z&sr=c&sp=r&sig=6TOIqiwDeLTbM1Y7Xu6%2F2%2F6ABUNqp7nPN7Lov1NhBSo%3D)
A "Show Customer" toggle switches to the customer-perspective view where applicable.
How data flows to the component
The LWC fetches data using a simple two-step query chain:
The component identifies the current record (VoiceCall or Task) and retrieves the associated
nbavs__CallReporting__crecord ID from the lookup field.It then queries
nbavs__NatterboxAI__crecords where thenbavs__Call_Reporting__clookup matches that Call Reporting ID.
Field naming conventions
Fields on nbavs__NatterboxAI__c are dynamically generated based on your AI Advisor prompt configuration. Each prompt creates fields with a unique hash suffix. The type of prompt determines which fields are created:
Rating prompts create two fields (same hash):
Rating_[hash]__c(Number) — the numeric score. LabelledRT-[Prompt Name]in Object Manager.Reason_[hash]__c(Long Text Area, 1000 chars) — the AI reasoning/feedback behind the score. LabelledRE-[Prompt Name]in Object Manager.
Free-text prompts create one field:
FreeText_[hash]__c(Long Text Area, 32768 chars) — the full text output. LabelledFT-[Prompt Name]in Object Manager.
For example, a rating prompt called "Agent Performance" with hash aa285ecdd562 creates:
Rating_aa285ecdd562__c→ contains the numeric score (e.g. 8)Reason_aa285ecdd562__c→ contains the AI's reasoning text
And a free-text prompt called "Summary - Advanced" with hash 68a6e76b4cdf creates:
FreeText_68a6e76b4cdf__c→ contains the full summary text
⚠️
Important — field API names are org-specific: The hash suffixes are unique to the prompt configuration in each org and will not match yours. Navigate to Setup > Object Manager > Natterbox AI > Fields & Relationships in your org to find the actual field API names for your prompts. Use the field label prefixes to identify them:
RT-= Rating (numeric score)
RE-= Reason (rating feedback text)
FT-= FreeText (free-text prompt output)
Introduction to Lightning Web Components (LWC)
Lightning Web Components (LWC) is a modern framework for building single-page applications on the Salesforce platform. It leverages web standards and provides a powerful way to create reusable components across different Salesforce applications.
Files overview
JavaScript file
Purpose: Contains the logic and functionality of the component.
Usage: Handles data fetching, event handling, and interaction with the Salesforce backend.
In this LWC:
Fetches AI results using an Apex method.
Initialises charts and handles user interactions.
Manages state variables and computed properties.
Key Variables:
recordId, aiResults, chart, isFlipped, recordType, chartJsInitialized, currentFeedback, isHoveringOverSegment, hoveredSegmentIndex, hoveredRatingName, hoveredRatingScore, isSegmentLocked, lockedSegmentIndex, lockedRatingName, lockedRatingScore, lockedFeedback, shouldInitializeChart, summaryImageUrl, nextStepsImageUrl, nbheaderUrl, displayRatingName, displayRatingScore, displayFeedback, isDisplayingSegment, toggleButtonLabel, toggleCard, initializeChart, determineRecordType, wiredAIResults, connectedCallback, renderedCallback
Dependencies:
Apex Controller:
getNatterboxAIResultsmethod.Static Resources: ChartJS, summaryImage, nextStepsImage, nbheader.
import { LightningElement, api, wire, track } from 'lwc';
import getNatterboxAIResults from '@salesforce/apex/NatterboxAIResultsController.getNatterboxAIResults';
import { loadScript } from 'lightning/platformResourceLoader';
import ChartJS from '@salesforce/resourceUrl/ChartJS';
import summaryImage from '@salesforce/resourceUrl/summaryImage';
import nextStepsImage from '@salesforce/resourceUrl/nextStepsImage';
import nbheader from '@salesforce/resourceUrl/nbheader';
export default class AiScorecard extends LightningElement {
@api recordId; // VoiceCall or Task record Id
@track aiResults = []; // Array to hold AI results
chart; // Chart instance
isFlipped = false; // Track the state of the card (flipped or not)
recordType = 'Unknown'; // Default record type
chartJsInitialized = false; // Flag to check if ChartJS is initialized
@track currentFeedback = ''; // Holds the feedback to display
@track isHoveringOverSegment = false; // Whether the user is hovering over a chart segment
@track hoveredSegmentIndex = null; // Index of the hovered segment
@track hoveredRatingName = ''; // Name of the hovered rating
@track hoveredRatingScore = null; // Score of the hovered rating
@track isSegmentLocked = false; // Whether a segment is locked
@track lockedSegmentIndex = null; // Index of the locked segment
@track lockedRatingName = ''; // Name of the locked rating
@track lockedRatingScore = null; // Score of the locked rating
@track lockedFeedback = ''; // Feedback of the locked rating
@track shouldInitializeChart = false; // Flag to indicate chart initialization after render
// Lifecycle hook that runs when the component is inserted into the DOM
connectedCallback() {
this.determineRecordType(); // Determine the record type based on the recordId
}
// Method to determine the record type based on the recordId prefix
determineRecordType() {
const recordPrefix = this.recordId.substring(0, 3);
if (recordPrefix === '0LQ') {
this.recordType = 'VoiceCall';
} else if (recordPrefix === '00T') {
this.recordType = 'Task';
} else {
this.recordType = 'Unknown';
}
console.log('Record Type:', this.recordType); // Debug log
}
// Wire service to fetch AI results based on the recordId
@wire(getNatterboxAIResults, { recordIds: '$recordId' })
wiredAIResults({ error, data }) {
if (data) {
console.log('AI Results:', data); // Debug log
this.aiResults = data;
if (this.chartJsInitialized && !this.isFlipped && this.aiResults.length > 0) {
this.initializeChart(); // Initialize the chart if conditions are met
}
} else if (error) {
console.error('Error fetching AI Results:', error); // Log any errors
}
}
// Lifecycle hook that runs after every render of the component
renderedCallback() {
if (this.chartJsInitialized) {
if (this.shouldInitializeChart && !this.isFlipped && this.aiResults.length > 0 && !this.chart) {
this.initializeChart(); // Initialize the chart if conditions are met
this.shouldInitializeChart = false; // Reset the flag
}
return;
}
this.chartJsInitialized = true;
// Load ChartJS library
Promise.all([
loadScript(this, ChartJS)
])
.then(() => {
if (!this.isFlipped && this.aiResults.length > 0) {
this.initializeChart(); // Initialize the chart if conditions are met
}
})
.catch(error => {
console.error('Error loading ChartJS', error); // Log any errors
});
}
// Method to initialize the chart
initializeChart() {
if (this.chart) {
this.chart.destroy(); // Destroy the old chart instance before creating a new one
this.chart = null;
}
if (this.aiResults.length === 0) {
console.warn('No AI Results to display.'); // Warn if there are no AI results
return;
}
const canvas = this.template.querySelector('.polar-area-chart');
if (!canvas) {
console.error('Canvas element not found'); // Error if canvas element is not found
return;
}
// sets max height and width for chart
canvas.style.maxWidth = '550px';
canvas.style.maxHeight = '550px';
const ctx = canvas.getContext('2d');
// Extract RATINGS (numeric scores) from Rating_[hash]__c fields
// ⚠️ REPLACE these field API names with YOUR org's actual field names.
// Find them at: Setup > Object Manager > Natterbox AI > Fields & Relationships
// Rating fields are labelled "RT-[Prompt Name]" and use the pattern Rating_[hash]__c
const ratings = [
this.aiResults[0].Rating_aa285ecdd562__c || 0, // e.g. RT-Agent Performance
this.aiResults[0].Rating_1cbb459955be__c || 0, // e.g. RT-Customer Experience
this.aiResults[0].Rating_22cbe76e5458__c || 0, // e.g. RT-Discovery & Needs Analysis
this.aiResults[0].Rating_0caf428f8229__c || 0, // e.g. RT-Bad Words Used
this.aiResults[0].Rating_e74913288a3f__c || 0, // e.g. RT-Escalation Risk Identification
this.aiResults[0].Rating_9b8d29e67be2__c || 0 // e.g. RT-Competitors Discussed
];
// Extract REASONING (feedback text) from Reason_[hash]__c fields
// Reason fields share the SAME hash as their corresponding Rating field
// They are labelled "RE-[Prompt Name]" in Object Manager
const feedbackArray = [
this.aiResults[0].Reason_aa285ecdd562__c || '', // RE-Agent Performance
this.aiResults[0].Reason_1cbb459955be__c || '', // RE-Customer Experience
this.aiResults[0].Reason_22cbe76e5458__c || '', // RE-Discovery & Needs Analysis
this.aiResults[0].Reason_0caf428f8229__c || '', // RE-Bad Words Used
this.aiResults[0].Reason_e74913288a3f__c || '', // RE-Escalation Risk Identification
this.aiResults[0].Reason_9b8d29e67be2__c || '' // RE-Competitors Discussed
];
// Define background colors for the chart segments
const backgroundColors = [
'rgb(255, 99, 132)', // Agent Performance
'rgb(75, 192, 192)', // Customer Experience
'rgb(255, 205, 86)', // Discovery & Needs Analysis
'rgb(201, 203, 207)', // Bad Words Used
'rgb(54, 162, 235)', // Escalation Risk Identification
'rgb(153, 102, 255)' // Competitors Discussed
];
// Data for the chart — update labels to match your prompt names
const data = {
labels: [
'Agent Performance',
'Customer Experience',
'Discovery & Needs Analysis',
'Bad Words Used',
'Escalation Risk Identification',
'Competitors Discussed'
],
datasets: [{
label: 'Ratings',
data: ratings,
backgroundColor: backgroundColors
}]
};
const self = this;
// Chart configuration
const config = {
type: 'polarArea',
data: data,
options: {
legend: {
display: false // Hide the legend
},
tooltips: {
enabled: false // Disable default tooltips
},
hover: {
onHover: function(event, chartElements) {
if (!self.isSegmentLocked) {
if (chartElements && chartElements.length) {
const index = chartElements[0]._index;
self.isHoveringOverSegment = true;
self.hoveredSegmentIndex = index;
self.hoveredRatingName = data.labels[index];
self.hoveredRatingScore = ratings[index];
self.currentFeedback = feedbackArray[index];
} else {
self.isHoveringOverSegment = false;
self.hoveredSegmentIndex = null;
self.hoveredRatingName = '';
self.hoveredRatingScore = null;
self.currentFeedback = '';
}
}
}
},
onClick: function(event, chartElements) {
if (chartElements && chartElements.length) {
const index = chartElements[0]._index;
if (self.isSegmentLocked && self.lockedSegmentIndex === index) {
// Unlock if clicking on the same segment
self.isSegmentLocked = false;
self.lockedSegmentIndex = null;
self.lockedRatingName = '';
self.lockedRatingScore = null;
self.lockedFeedback = '';
self.isHoveringOverSegment = false;
} else {
// Lock the segment
self.isSegmentLocked = true;
self.lockedSegmentIndex = index;
self.lockedRatingName = data.labels[index];
self.lockedRatingScore = ratings[index];
self.lockedFeedback = feedbackArray[index];
// Update the hover variables as well
self.isHoveringOverSegment = true;
self.hoveredSegmentIndex = index;
self.hoveredRatingName = data.labels[index];
self.hoveredRatingScore = ratings[index];
self.currentFeedback = feedbackArray[index];
}
} else {
// Clicked outside segments, unlock
self.isSegmentLocked = false;
self.lockedSegmentIndex = null;
self.lockedRatingName = '';
self.lockedRatingScore = null;
self.lockedFeedback = '';
self.isHoveringOverSegment = false;
self.hoveredSegmentIndex = null;
self.hoveredRatingName = '';
self.hoveredRatingScore = null;
self.currentFeedback = '';
}
},
scale: {
ticks: {
display: true,
stepSize: 2,
maxTicksLimit: 10,
backdropColor: 'rgba(255, 255, 255, 1)'
}
}
},
plugins: [{
afterDatasetsDraw: function(chart) {
if (chart.scale) {
chart.scale.draw(chart.ctx); // Ensure the scale is drawn after datasets
}
}
}]
};
// Create a new Chart instance
this.chart = new Chart(ctx, config);
}
// Getter methods for FREE-TEXT prompts (FreeText_[hash]__c fields)
// These are separate prompts that produce text-only output (labelled FT- in Object Manager)
// ⚠️ Replace these with YOUR org's actual FreeText field API names
get callResolution() {
// Maps to a Reason field from a rating prompt (e.g. RE-AI Call Deflection Identifier)
return this.aiResults.length > 0 ? this.aiResults[0].Reason_aa10d4107ce6__c : '';
}
get overallCallRating() {
// Maps to a Rating field (e.g. RT-Customer Sentiment)
return this.aiResults.length > 0 ? this.aiResults[0].Rating_50d1f386c760__c : '';
}
get callSummary() {
// Maps to a FreeText field (e.g. FT-Summary - Advanced)
return this.aiResults.length > 0 ? this.aiResults[0].FreeText_68a6e76b4cdf__c : '';
}
get nextSteps() {
// Maps to a FreeText field (e.g. FT-Next Best Action)
return this.aiResults.length > 0 ? this.aiResults[0].FreeText_19c4c22d757c__c : '';
}
// Rating getters for the chart legend display
// These pull from Rating_[hash]__c fields (numeric scores)
get rating1() {
return this.aiResults.length > 0 ? this.aiResults[0].Rating_aa285ecdd562__c : '';
}
get rating2() {
return this.aiResults.length > 0 ? this.aiResults[0].Rating_1cbb459955be__c : '';
}
get rating3() {
return this.aiResults.length > 0 ? this.aiResults[0].Rating_22cbe76e5458__c : '';
}
get rating4() {
return this.aiResults.length > 0 ? this.aiResults[0].Rating_0caf428f8229__c : '';
}
get rating5() {
return this.aiResults.length > 0 ? this.aiResults[0].Rating_e74913288a3f__c : '';
}
get rating6() {
return this.aiResults.length > 0 ? this.aiResults[0].Rating_9b8d29e67be2__c : '';
}
// Getter methods to retrieve resource URLs
get summaryImageUrl() {
return summaryImage;
}
get nextStepsImageUrl() {
return nextStepsImage;
}
get nbheaderUrl() {
return nbheader;
}
// Getters for displaying data based on hover or lock state
get displayRatingName() {
return this.isSegmentLocked ? this.lockedRatingName : this.hoveredRatingName;
}
get displayRatingScore() {
return this.isSegmentLocked ? this.lockedRatingScore : this.hoveredRatingScore;
}
get displayFeedback() {
return this.isSegmentLocked ? this.lockedFeedback : this.currentFeedback;
}
get isDisplayingSegment() {
return this.isSegmentLocked || this.isHoveringOverSegment;
}
// Method to toggle the card state (flipped or not)
toggleCard() {
this.isFlipped = !this.isFlipped;
if (this.isFlipped) {
// Flipping to the summary view; destroy the chart
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
} else {
// Flipping back to the chart view; set a flag to initialize the chart in renderedCallback
this.shouldInitializeChart = true;
}
}
// Getter for the button label based on the card state
get toggleButtonLabel() {
return this.isFlipped ? 'View Scorecard' : 'View Summary & Next Steps';
}
}Adjusting JavaScript
Adjusting the LWC code to include different criteria for customer call rating
When changing the criteria used to rate calls, the primary areas to adjust in your JavaScript code will be where data is retrieved from the aiResults object and how it's displayed on the chart. Below are the necessary adjustments, with an example for adding two more rating prompts.
Key variables to update
ratings Array: This array holds the numeric scores. Add new entries for additional
Rating_[hash]__cfields from your org.feedbackArray: This array holds the reasoning text for each rating. Add the corresponding
Reason_[hash]__cfields (same hash as the Rating field).backgroundColors: Add distinct colours for the new ratings to distinguish them on the chart.
labels: Add new labels to the chart for the additional criteria.
Example code snippet: adding two more rating criteria
// Inside initializeChart()
// After finding your new prompt's field API names in Object Manager,
// add them to the arrays:
// Ratings use Rating_[hash]__c fields (labelled RT- in Object Manager)
const ratings = [
this.aiResults[0].Rating_aa285ecdd562__c || 0, // Agent Performance
this.aiResults[0].Rating_1cbb459955be__c || 0, // Customer Experience
this.aiResults[0].Rating_22cbe76e5458__c || 0, // Discovery & Needs Analysis
this.aiResults[0].Rating_0caf428f8229__c || 0, // Bad Words Used
this.aiResults[0].Rating_e74913288a3f__c || 0, // Escalation Risk Identification
this.aiResults[0].Rating_9b8d29e67be2__c || 0, // Competitors Discussed
this.aiResults[0].Rating_abc123def456__c || 0, // Problem-Solving (new — use YOUR hash)
this.aiResults[0].Rating_789ghi012jkl__c || 0 // Patience (new — use YOUR hash)
];
// Feedback uses Reason_[hash]__c fields (labelled RE- in Object Manager)
// The hash is the SAME as the corresponding Rating field for that prompt
const feedbackArray = [
this.aiResults[0].Reason_aa285ecdd562__c || '', // RE-Agent Performance
this.aiResults[0].Reason_1cbb459955be__c || '', // RE-Customer Experience
this.aiResults[0].Reason_22cbe76e5458__c || '', // RE-Discovery & Needs Analysis
this.aiResults[0].Reason_0caf428f8229__c || '', // RE-Bad Words Used
this.aiResults[0].Reason_e74913288a3f__c || '', // RE-Escalation Risk Identification
this.aiResults[0].Reason_9b8d29e67be2__c || '', // RE-Competitors Discussed
this.aiResults[0].Reason_abc123def456__c || '', // RE-Problem-Solving (new)
this.aiResults[0].Reason_789ghi012jkl__c || '' // RE-Patience (new)
];
const backgroundColors = [
'rgb(255, 99, 132)', // Agent Performance
'rgb(75, 192, 192)', // Customer Experience
'rgb(255, 205, 86)', // Discovery & Needs Analysis
'rgb(201, 203, 207)', // Bad Words Used
'rgb(54, 162, 235)', // Escalation Risk Identification
'rgb(153, 102, 255)', // Competitors Discussed
'rgb(255, 159, 64)', // Problem-Solving (new)
'rgb(128, 128, 255)' // Patience (new)
];
const data = {
labels: [
'Agent Performance',
'Customer Experience',
'Discovery & Needs Analysis',
'Bad Words Used',
'Escalation Risk Identification',
'Competitors Discussed',
'Problem-Solving', // New label
'Patience' // New label
],
datasets: [{
label: 'Ratings',
data: ratings,
backgroundColor: backgroundColors
}]
};Explanation of changes
Ratings Array: Uses
Rating_[hash]__cfields (labelledRT-in Object Manager). Each field contains the numeric score for that prompt.Feedback Array: Uses
Reason_[hash]__cfields (labelledRE-in Object Manager). The hash suffix is the same as the corresponding Rating field — each rating prompt generates both aRating_and aReason_field with the same hash.Chart Labels: Update the
labelsarray to include human-readable names for each rating criterion. These labels appear on the polar area chart.Background Colours: Add distinct colours for the new ratings to differentiate them visually on the chart.
Where to modify data sources (Apex and LWC communication)
Backend/Apex Changes: Ensure that the new fields (both
Rating_[hash]__candReason_[hash]__c) are included in the SOQL SELECT statement in thegetNatterboxAIResultsApex method.LWC Updates: Add new getter methods for any ratings you want to display in the HTML legend (e.g.,
get rating7(),get rating8()).
By adjusting the code in these sections, you can easily extend the chart to handle additional rating criteria while keeping the same structure and logic.
HTML file
Purpose: Defines the structure and layout of the component's user interface.
Usage: Contains HTML tags and LWC tags to create visual elements.
In this LWC:
Displays overall call rating and AI call outcome.
Includes a toggle button to switch views.
Shows a polar area chart for call ratings.
Conditionally renders sections based on user interaction.
Displays summary, next steps, and individual ratings with feedback.
Shows images and dynamically computed properties.
Variables from JavaScript:
callResolution, overallCallRating, toggleButtonLabel, isFlipped, callSummary, nextSteps, rating1, rating2, rating3, rating4, rating5, rating6, summaryImageUrl, nextStepsImageUrl, nbheaderUrl, displayRatingName, displayRatingScore, displayFeedback, toggleCard
Variables from CSS:
.custom-card-container, .small-chart
aiScorecard.html with detailed comments
<template>
<!-- Main container for the custom card component -->
<div class="custom-card-container">
<!-- Lightning card element that provides a consistent UI structure -->
<lightning-card>
<!-- Padding added around the content for spacing -->
<div class="slds-var-p-around_medium">
<!-- Header Section: Contains an image (e.g., Natterbox logo or header) -->
<div class="slds-text-align_center slds-var-m-bottom_medium">
<img src={nbheaderUrl} alt="Natterbox Header" style="max-width:300px;" />
</div>
<hr class="slds-var-m-vertical_medium"/>
<!-- Top Section: Displays the AI Call Outcome -->
<div class="slds-text-heading_small slds-text-align_center slds-var-m-bottom_medium">
AI Call Outcome: <span>{callResolution}</span>
</div>
<hr class="slds-var-m-vertical_medium"/>
<!-- Top Section: Displays the overall call rating -->
<div class="slds-text-heading_small slds-var-m-bottom_medium slds-text-align_center">
<strong> Overall Call Rating: <span>{overallCallRating}</span></strong>
</div>
<hr class="slds-var-m-vertical_medium"/>
<!-- Toggle Button Section -->
<div class="slds-text-align_center slds-var-m-bottom_medium">
<lightning-button label={toggleButtonLabel} onclick={toggleCard} size="small" class="small-button"></lightning-button>
</div>
<hr class="slds-var-m-vertical_medium"/>
<!-- Flipped view: Summary and Next Steps -->
<template if:true={isFlipped}>
<div class="slds-text-align_center slds-var-m-bottom_medium">
<img src={summaryImageUrl} alt="Summary Image" style="width:50px;height:50px;" />
<div>
<strong>Summary:</strong>
<div>
<lightning-formatted-text value={callSummary}></lightning-formatted-text>
</div>
</div>
</div>
<hr class="slds-var-m-vertical_medium"/>
<div class="slds-text-align_center slds-var-m-bottom_medium">
<img src={nextStepsImageUrl} alt="Next Steps Image" style="width:50px;height:50px;" />
<div>
<strong>Next Steps:</strong>
<div>
<lightning-formatted-text value={nextSteps}></lightning-formatted-text>
</div>
</div>
</div>
</template>
<!-- Default view: Polar Area Chart -->
<template if:false={isFlipped}>
<div class="slds-var-m-bottom_medium slds-text-align_center">
<div class="chart-container" style="position:relative;display:flex;justify-content:center;align-items:center;">
<canvas class="polar-area-chart small-chart" lwc:dom="manual"></canvas>
</div>
</div>
<hr class="slds-var-m-vertical_medium"/>
<!-- Legend (shown when no segment is selected) -->
<template if:false={isDisplayingSegment}>
<div class="slds-text-heading_small slds-text-align_center slds-var-m-bottom_medium" style="font-size:14.5px;">
<strong>Natterbox AI Call Ratings</strong>
</div>
<hr class="slds-var-m-vertical_medium"/>
<div class="slds-grid slds-wrap slds-var-m-bottom_medium slds-grid_align-center" style="font-size:14.5px;">
<!-- Left Column -->
<div class="slds-col slds-shrink slds-text-align_left slds-var-m-right_small slds-var-m-right_medium">
<ul class="slds-list_vertical slds-m-around_none">
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating1}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(255, 99, 132);width:14.5px;height:14.5px;"></span>
Agent Performance
</li>
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating2}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(75, 192, 192);width:14.5px;height:14.5px;"></span>
Customer Experience
</li>
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating3}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(255, 205, 86);width:14.5px;height:14.5px;"></span>
Discovery & Needs Analysis
</li>
</ul>
</div>
<!-- Right Column -->
<div class="slds-col slds-shrink slds-text-align_left slds-var-m-left_small slds-var-m-left_medium">
<ul class="slds-list_vertical slds-m-around_none">
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating4}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(201, 203, 207);width:14.5px;height:14.5px;"></span>
Bad Words Used
</li>
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating5}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(54, 162, 235);width:14.5px;height:14.5px;"></span>
Escalation Risk Identification
</li>
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating6}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(153, 102, 255);width:14.5px;height:14.5px;"></span>
Competitors Discussed
</li>
</ul>
</div>
</div>
</template>
<!-- Segment detail (shown on hover/click) -->
<template if:true={isDisplayingSegment}>
<div class="slds-text-heading_small slds-text-align_center slds-var-m-bottom_medium" style="white-space:nowrap;">
<strong>{displayRatingName} Rating: {displayRatingScore}</strong>
</div>
<hr class="slds-var-m-vertical_medium"/>
<div class="slds-text-align_center slds-var-m-around_medium">
<div class="slds-var-m-top_small" style="font-size:14.5px;">
<lightning-formatted-text value={displayFeedback}></lightning-formatted-text>
</div>
</div>
</template>
</template>
</div>
</lightning-card>
</div>
</template>Adjusting the HTML file
To add new rating criteria to the HTML legend, add new <li> items to the left or right column. Each item needs the getter reference and a matching background colour.
Example: adding two more ratings to the legend
<!-- Add to Left Column -->
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating7}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(255, 159, 64);width:14.5px;height:14.5px;"></span>
Problem-Solving
</li>
<!-- Add to Right Column -->
<li class="slds-var-m-bottom_small" style="white-space:nowrap;font-size:14.5px;">
<strong>{rating8}</strong>
<span class="slds-show_inline-block slds-var-m-left_x-small slds-var-m-right_x-small" style="background-color:rgb(128, 128, 255);width:14.5px;height:14.5px;"></span>
Patience
</li>Explanation of changes
Left Column Adjustments: Add a new <li> item with the getter (e.g.
{rating7}) and a matching background colour from yourbackgroundColorsarray.Right Column Adjustments: Same pattern — add the getter and matching colour.
Dynamic Values: The new getters (e.g.
{rating7},{rating8}) are bound to the correspondingRating_[hash]__cfields returned from Apex.Consistent Styling: Keep the same SLDS classes and inline styles for visual consistency.
XML configuration file
Purpose: Defines the metadata for the component.
Usage: Specifies where the component can be used and which objects it supports.
In this LWC:
Specifies that the component is exposed and can be used on VoiceCall and Task Records.
Key Variables:
apiVersion, isExposed, targets, targetConfig, objects
aiScorecard.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>58.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>VoiceCall</object>
<object>Task</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>CSS file
Purpose: Defines the styling and appearance of the component.
Usage: Contains CSS rules to style the HTML elements.
In this LWC:
Styles the component for a consistent and visually appealing appearance outside of the SLDS prebuilt classes being used.
Key Variables:
.custom-card-container, .small-chart
.custom-card-container {
overflow: visible;
}Apex controller
Purpose: Provides server-side logic and data fetching capabilities.
Usage: Contains methods to interact with Salesforce data and return it to the LWC.
In this LWC:
Fetches data from the
nbavs__NatterboxAI__cobject for the AI Scorecard component.
Key Method:
getNatterboxAIResults
Objects and Fields:
VoiceCall:
callreporting__c(Lookup tonbavs__CallReporting__c)Task:
nbavs__call_reporting__c(Lookup tonbavs__CallReporting__c)nbavs__NatterboxAI__c: queried vianbavs__Call_Reporting__clookup. Contains:Rating_[hash]__c— numeric score (from rating prompts)Reason_[hash]__c— reasoning text (from rating prompts, same hash as Rating)FreeText_[hash]__c— text output (from free-text prompts)
public with sharing class NatterboxAIResultsController {
@AuraEnabled(cacheable=true)
public static List<nbavs__NatterboxAI__c> getNatterboxAIResults(List<Id> recordIds) {
try {
// Determine the record type based on the prefix
String recordPrefix = String.valueOf(recordIds[0]).substring(0, 3);
List<Id> callReportingIds = new List<Id>();
if (recordPrefix == '0LQ') {
// Handle VoiceCall records
List<VoiceCall> voiceCalls = [
SELECT callreporting__c
FROM VoiceCall
WHERE Id IN :recordIds
];
for (VoiceCall vc : voiceCalls) {
if (vc.callreporting__c != null) {
callReportingIds.add(vc.callreporting__c);
}
}
} else if (recordPrefix == '00T') {
// Handle Task records
List<Task> tasks = [
SELECT nbavs__call_reporting__c
FROM Task
WHERE Id IN :recordIds
];
for (Task t : tasks) {
if (t.nbavs__call_reporting__c != null) {
callReportingIds.add(t.nbavs__call_reporting__c);
}
}
} else {
throw new AuraHandledException('Unsupported record type');
}
System.debug('Call Reporting IDs: ' + callReportingIds);
// Query Natterbox AI records directly via the Call Reporting lookup
// ⚠️ REPLACE these field API names with YOUR org's actual field names.
// Find them at: Setup > Object Manager > Natterbox AI > Fields & Relationships
//
// Field label prefixes tell you the type:
// RT- = Rating (numeric score) → Rating_[hash]__c
// RE- = Reason (feedback text) → Reason_[hash]__c
// FT- = FreeText (text output) → FreeText_[hash]__c
//
// Rating prompts produce BOTH a Rating_ and Reason_ field with the same hash.
// Free-text prompts produce only a FreeText_ field.
List<nbavs__NatterboxAI__c> aiResults = [
SELECT
// Rating scores (RT- fields)
Rating_aa285ecdd562__c, Rating_1cbb459955be__c, Rating_22cbe76e5458__c,
Rating_0caf428f8229__c, Rating_e74913288a3f__c, Rating_9b8d29e67be2__c,
Rating_50d1f386c760__c,
// Reasoning text (RE- fields — same hash as corresponding Rating)
Reason_aa285ecdd562__c, Reason_1cbb459955be__c, Reason_22cbe76e5458__c,
Reason_0caf428f8229__c, Reason_e74913288a3f__c, Reason_9b8d29e67be2__c,
Reason_aa10d4107ce6__c,
// Free-text outputs (FT- fields — separate prompts)
FreeText_68a6e76b4cdf__c, FreeText_19c4c22d757c__c
FROM nbavs__NatterboxAI__c
WHERE nbavs__Call_Reporting__c IN :callReportingIds
];
System.debug('AI Results: ' + aiResults);
return aiResults;
} catch (AuraHandledException e) {
System.debug('Caught AuraHandledException: ' + e.getMessage());
throw e;
} catch (Exception e) {
System.debug('Caught Exception: ' + e.getMessage());
throw new AuraHandledException('Error fetching AI results: ' + e.getMessage());
}
}
}💡
Tip: The field names in the SELECT statement above are examples from a demo org. Replace them with the actual field API names from your org's
nbavs__NatterboxAI__cobject. Navigate to Setup > Object Manager > Natterbox AI > Fields & Relationships to find your field names. Remember:
Rating prompts generate two fields:
Rating_[hash]__c(number) andReason_[hash]__c(text) with the same hashFree-text prompts generate one field:
FreeText_[hash]__c(text)Field labels use
RT-,RE-, orFT-prefixes followed by the prompt name
Key differences from the previous version
The previous version of this component used a custom object (Natterbox_AI_Results__c) that required a Salesforce Flow to populate. The updated approach queries the managed nbavs__NatterboxAI__c object directly — eliminating the need for:
A custom object and custom fields
A Salesforce Flow to copy data between objects
The intermediate lookup hop through
nbavs__CallReporting__c.Natterbox_AI_Results__c
The query chain is now simpler: VoiceCall/Task → nbavs__CallReporting__c → nbavs__NatterboxAI__c (via the nbavs__Call_Reporting__c lookup on the Natterbox AI object).
Adjusting Apex controller
To include additional rating or free-text prompts, find the new field's API name in Object Manager and add it to the SOQL query. Remember that each rating prompt needs both its Rating_[hash]__c and Reason_[hash]__c fields:
List<nbavs__NatterboxAI__c> aiResults = [
SELECT
// Existing rating scores
Rating_aa285ecdd562__c, Rating_1cbb459955be__c, Rating_22cbe76e5458__c,
Rating_0caf428f8229__c, Rating_e74913288a3f__c, Rating_9b8d29e67be2__c,
Rating_50d1f386c760__c,
// New rating scores (use YOUR hashes)
Rating_abc123def456__c, Rating_789ghi012jkl__c,
// Existing reasoning text
Reason_aa285ecdd562__c, Reason_1cbb459955be__c, Reason_22cbe76e5458__c,
Reason_0caf428f8229__c, Reason_e74913288a3f__c, Reason_9b8d29e67be2__c,
Reason_aa10d4107ce6__c,
// New reasoning text (same hash as the new Rating fields)
Reason_abc123def456__c, Reason_789ghi012jkl__c,
// Existing free-text outputs
FreeText_68a6e76b4cdf__c, FreeText_19c4c22d757c__c
FROM nbavs__NatterboxAI__c
WHERE nbavs__Call_Reporting__c IN :callReportingIds
];Apex test class
Purpose: Ensures the Apex Controller works as expected.
Usage: Contains test methods to validate the functionality of the Apex Controller.
In this LWC:
Validates the functionality of the NatterboxAIResultsController.
Test Methods:
testGetNatterboxAIResults_VoiceCall
testGetNatterboxAIResults_Task
Objects and Fields:
VoiceCall:
callreporting__c(Lookup tonbavs__CallReporting__c)Task:
nbavs__call_reporting__c(Lookup tonbavs__CallReporting__c)nbavs__NatterboxAI__c: queried vianbavs__Call_Reporting__clookup
@isTest
public class NatterboxAIResultsControllerTest {
@testSetup
static void setupTestData() {
// Create test Call Reporting record
nbavs__CallReporting__c callReporting1 = new nbavs__CallReporting__c();
insert callReporting1;
// Create test Natterbox AI record linked to Call Reporting
// ⚠️ REPLACE these field API names with YOUR org's actual field names.
// Find them at: Setup > Object Manager > Natterbox AI > Fields & Relationships
//
// Rating prompts create TWO fields with the same hash:
// Rating_[hash]__c = numeric score
// Reason_[hash]__c = reasoning text
// Free-text prompts create ONE field:
// FreeText_[hash]__c = text output
nbavs__NatterboxAI__c aiResult1 = new nbavs__NatterboxAI__c(
nbavs__Call_Reporting__c = callReporting1.Id,
// Rating scores (RT- fields)
Rating_aa285ecdd562__c = 8, // RT-Agent Performance
Rating_1cbb459955be__c = 7, // RT-Customer Experience
Rating_22cbe76e5458__c = 9, // RT-Discovery & Needs Analysis
Rating_0caf428f8229__c = 6, // RT-Bad Words Used
Rating_e74913288a3f__c = 9, // RT-Escalation Risk Identification
Rating_9b8d29e67be2__c = 7, // RT-Competitors Discussed
Rating_50d1f386c760__c = 8, // RT-Customer Sentiment (Overall)
// Reasoning text (RE- fields — same hash as Rating)
Reason_aa285ecdd562__c = 'Agent demonstrated strong performance throughout the call.',
Reason_1cbb459955be__c = 'Customer appeared satisfied with the resolution provided.',
Reason_22cbe76e5458__c = 'Good discovery questions asked to understand needs.',
Reason_0caf428f8229__c = 'No inappropriate language detected.',
Reason_e74913288a3f__c = 'Low escalation risk — issue resolved at first contact.',
Reason_9b8d29e67be2__c = 'No competitor mentions during the call.',
Reason_aa10d4107ce6__c = 'Call resolved successfully without deflection.',
// Free-text outputs (FT- fields — separate prompts)
FreeText_68a6e76b4cdf__c = 'Customer called regarding a billing query. Agent resolved the issue.',
FreeText_19c4c22d757c__c = 'Follow-up required to confirm credit has been applied.'
);
insert aiResult1;
// Create test VoiceCall
VoiceCall voiceCall1 = new VoiceCall(
CallCenterId = '04v8d000000oOydAAE', // Replace with a valid CallCenterId from your org
VendorType = 'ContactCenter',
CallStartDateTime = System.now(),
CallEndDateTime = System.now().addMinutes(5),
FromPhoneNumber = '1234567890',
ToPhoneNumber = '0987654321',
CallType = 'Inbound',
callreporting__c = callReporting1.Id
);
insert voiceCall1;
// Create test Task
Task task1 = new Task(
Subject = 'Test Task',
Status = 'Not Started',
Priority = 'Normal',
nbavs__call_reporting__c = callReporting1.Id
);
insert task1;
}
@isTest
static void testGetNatterboxAIResults_VoiceCall() {
VoiceCall testVoiceCall = [SELECT Id FROM VoiceCall LIMIT 1];
Test.startTest();
List<nbavs__NatterboxAI__c> results = NatterboxAIResultsController.getNatterboxAIResults(new List<Id> { testVoiceCall.Id });
Test.stopTest();
System.assertNotEquals(null, results, 'Results should not be null');
System.assertEquals(1, results.size(), 'There should be 1 result');
}
@isTest
static void testGetNatterboxAIResults_Task() {
Task testTask = [SELECT Id FROM Task LIMIT 1];
Test.startTest();
List<nbavs__NatterboxAI__c> results = NatterboxAIResultsController.getNatterboxAIResults(new List<Id> { testTask.Id });
Test.stopTest();
System.assertNotEquals(null, results, 'Results should not be null');
System.assertEquals(1, results.size(), 'There should be 1 result');
}
}ChartJS
Purpose: Provides the functionality to create and display charts.
Usage: Contains the JavaScript library necessary for rendering various types of charts.
In this LWC:
Used to create and display a polar area chart for visualising AI call ratings.
Key Variables and Methods:
Variables:
Chart: The main ChartJS object used to create charts.
ctx: The context of the canvas element where the chart will be rendered.
data: The data object containing labels and datasets for the chart.
config: The configuration object for the chart, including type, data, and options.
ratings: An array containing the rating values from
Rating_[hash]__cfields.backgroundColors: An array containing the colours for each segment of the chart.
feedbackArray: An array containing reasoning text from
Reason_[hash]__cfields.
Methods:
initializeChart(): Initialises and renders the polar area chart using ChartJS.
destroy(): Destroys the existing chart instance before creating a new one.
onHover: Handles hover events on the chart segments to display rating details and reasoning.
onClick: Handles click events on the chart segments to lock the rating details.
Dependencies:
JavaScript File: Implements the logic to initialise and manage the chart.
HTML File: Contains the <canvas> element where the chart is rendered.
Static Resource: The ChartJS library must be uploaded as a static resource in Salesforce.
Summary of file functionality
The various components of the Natterbox AI Scorecard LWC work together to create an interactive user experience. The JavaScript file handles the logic and data fetching by leveraging Apex methods to retrieve AI prompt results from the nbavs__NatterboxAI__c object. Rating prompts provide both a numeric score (Rating_[hash]__c) and reasoning text (Reason_[hash]__c), while free-text prompts provide a single text output (FreeText_[hash]__c). This data is then dynamically inserted into the HTML file to construct the user interface. The CSS file ensures that the interface is visually appealing and consistent, while the XML configuration file defines the component's metadata and specifies its usage context. Together, these files create a cohesive system that presents AI Advisor results in an engaging, interactive format on Task and VoiceCall records.
Deployment guide
This section provides detailed steps for deploying the AI Scorecard component (aiScorecard) into a Salesforce environment.
Prerequisites
Natterbox App package installed (v1.348 or later — Single Object enabled)
AI Advisor configured and generating prompt results on the
nbavs__NatterboxAI__cobjectSalesforce Sandbox environment
Visual Studio Code (VS Code) with Salesforce Extensions
Salesforce CLI
Necessary permissions to deploy components in Salesforce
Step 1: Setting up Visual Studio Code
Download Visual Studio Code: Go to the Visual Studio Code website and download the version for your operating system.
Install Visual Studio Code: Follow the installation instructions for your OS (Windows .exe, macOS .dmg, or Linux package).
Install Salesforce Extension Pack: Open VS Code, go to Extensions (Ctrl+Shift+X / Cmd+Shift+X), search for "Salesforce Extension Pack", and click Install.
Install Salesforce CLI: Download from the Salesforce CLI download page and follow the installation instructions for your OS.
Step 2: Connecting to Salesforce Sandbox
Open Terminal in VS Code: Go to View > Terminal.
Authenticate to Salesforce:
sfdx force:auth:web:login -r https://test.salesforce.com -a MySandboxAliasFollow the prompts to log in to your Salesforce sandbox.
Create a Project:
sfdx force:project:create -n AiCallRating
cd AiCallRatingStep 3: Deploying the LWC files
Upload Static Resources to Salesforce:
Log in to your Salesforce sandbox.
Go to Setup > Static Resources.
Click New and upload each static resource one by one: ChartJS, summaryImage, nextStepsImage, nbheader.
Ensure each static resource is set to Cache Control: Public.
Create LWC Component:
sfdx force:lightning:component:create -n aiScorecard -d force-app/main/default/lwcAdd HTML, CSS, JavaScript, and XML Files: Replace the content of the generated files with the code provided in this guide (aiScorecard.html, aiScorecard.js, aiScorecard.js-meta.xml, aiScorecard.css).
Push the Component to Salesforce:
sfdx force:source:deploy -p force-app/main/default/lwc -u MySandboxAliasPushing Apex classes in VS Code:
sfdx force:source:deploy -p force-app/main/default/classes -u MySandboxAliasLWC Folder Structure with Apex classes included:
AiCallRating/
│
├─── force-app/
│ ├─── main/
│ │ ├─── default/
│ │ │ ├─── classes/
│ │ │ │ ├─── NatterboxAIResultsController.cls
│ │ │ │ ├─── NatterboxAIResultsControllerTest.cls
│ │ │ │
│ │ │ ├─── lwc/
│ │ │ │ ├─── aiScorecard/
│ │ │ │ │ ├─── aiScorecard.html
│ │ │ │ │ ├─── aiScorecard.js
│ │ │ │ │ ├─── aiScorecard.css
│ │ │ │ │ ├─── aiScorecard.js-meta.xmlStep 4: Deploying Apex code in Salesforce
Open Developer Console: Log in to your Salesforce sandbox. Click on the gear icon and select Developer Console.
Create Apex Class: Go to File > New > Apex Class. Name it NatterboxAIResultsController. Copy and paste the provided Apex code into the class and save.
Create Apex Test Class: Go to File > New > Apex Class. Name it NatterboxAIResultsControllerTest. Copy and paste the provided test class code into the class and save.
Run Apex Tests: In the Developer Console, go to Test > New Run. Select NatterboxAIResultsControllerTest and run the tests to ensure they pass.
Step 5: Deploying to production
Create a Change Set: Log in to your Salesforce sandbox. Go to Setup > Change Sets. Create a new outbound change set. Add the following components:
LWC component: aiScorecard
Apex class: NatterboxAIResultsController
Apex test class: NatterboxAIResultsControllerTest
Static Resources: ChartJS, summaryImage, nextStepsImage, nbheader
Upload Change Set to Production: Upload the change set to your production environment.
Validate Change Set in Production: Log in to production. Go to Setup > Change Sets. Find the uploaded change set and click Validate. During validation, ensure that only the Apex test class NatterboxAIResultsControllerTest is run.
Deploy Change Set in Production: After successful validation, go back to the change set and click Deploy. Ensure that only the Apex test class NatterboxAIResultsControllerTest is run during the deployment.
Additional details and resources
Visual Studio Code Documentation: Getting Started with Visual Studio Code
Salesforce CLI Documentation: Salesforce CLI Setup Guide
Salesforce Extensions for VS Code: Salesforce Extensions Documentation
Dependencies
Ensure that the necessary Salesforce objects (
VoiceCall,Task,nbavs__NatterboxAI__c,nbavs__CallReporting__c) are available in the target environment.Field names on
nbavs__NatterboxAI__care org-specific. Each AI Advisor prompt generates fields with a unique hash suffix. Rating prompts create bothRating_[hash]__candReason_[hash]__c(same hash). Free-text prompts createFreeText_[hash]__c. The code in this guide uses example hashes — you must replace them with your org's actual field API names.
Troubleshooting Apex test
Ensure that the necessary Salesforce objects (
VoiceCall,Task,nbavs__NatterboxAI__c,nbavs__CallReporting__c) are available in the target environment.If the test class fails, check that the field API names used in the test data match those available on your
nbavs__NatterboxAI__cobject. Navigate to Setup > Object Manager > Natterbox AI > Fields & Relationships to verify.Field label prefixes identify the type:
RT-= Rating (number),RE-= Reason (text),FT-= FreeText (text). A single rating prompt produces both anRT-andRE-field with the same hash.The
CallCenterIdvalue in the test VoiceCall record must be replaced with a valid Call Center ID from your org.