---
title: "AI ScoreCard - LWC"
slug: "ai-scorecard-lwc"
updated: 2026-06-12T15:24:32Z
published: 2026-06-12T15:24:32Z
canonical: "docs.natterbox.com/ai-scorecard-lwc"
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.natterbox.com/llms.txt
> Use this file to discover all available pages before exploring further.
# AI ScoreCard - LWC
> [!NOTE]
> This guide is intended for advanced Natterbox admins. Further customisations of this solution might not be supported.
> [!WARNING]
> 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](/docs/natterbox-ai-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-10-12T06%3A27%3A04Z&se=2026-10-12T07%3A27%3A04Z&sr=c&sp=r&sig=BjyzXAC4jkSl7oq9YLIei3gy%2FkluyL6sFnrXDjUMglM%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-10-12T06%3A27%3A04Z&se=2026-10-12T07%3A27%3A04Z&sr=c&sp=r&sig=BjyzXAC4jkSl7oq9YLIei3gy%2FkluyL6sFnrXDjUMglM%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:
1. The component identifies the current record (VoiceCall or Task) and retrieves the associated `nbavs__CallReporting__c` record ID from the lookup field.
2. It then queries `nbavs__NatterboxAI__c` records where the `nbavs__Call_Reporting__c` lookup 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. Labelled `RT-[Prompt Name]` in Object Manager.
- `Reason_[hash]__c` (Long Text Area, 1000 chars) — the AI reasoning/feedback behind the score. Labelled `RE-[Prompt Name]` in Object Manager.
**Free-text prompts** create **one** field:
- `FreeText_[hash]__c` (Long Text Area, 32768 chars) — the full text output. Labelled `FT-[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:** `getNatterboxAIResults` method.
- **Static Resources:** ChartJS, summaryImage, nextStepsImage, nbheader.
```javascript
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]__c` fields from your org.
- **feedbackArray**: This array holds the reasoning text for each rating. Add the corresponding `Reason_[hash]__c` fields (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
```javascript
// 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
1. **Ratings Array**: Uses `Rating_[hash]__c` fields (labelled `RT-` in Object Manager). Each field contains the numeric score for that prompt.
2. **Feedback Array**: Uses `Reason_[hash]__c` fields (labelled `RE-` in Object Manager). The hash suffix is the **same** as the corresponding Rating field — each rating prompt generates both a `Rating_` and a `Reason_` field with the same hash.
3. **Chart Labels**: Update the `labels` array to include human-readable names for each rating criterion. These labels appear on the polar area chart.
4. **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]__c` and `Reason_[hash]__c`) are included in the SOQL SELECT statement in the `getNatterboxAIResults` Apex 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**
```xml