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.

Use SSO to log in and access the Help Center, where you can create and manage your support tickets to reduce your resolution times.

AI ScoreCard - LWC

Prev Next

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.

  • 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.

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.

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

// 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

<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

  1. Left Column Adjustments: Add a new <li> item with the getter (e.g. {rating7}) and a matching background colour from your backgroundColors array.

  2. Right Column Adjustments: Same pattern — add the getter and matching colour.

  3. Dynamic Values: The new getters (e.g. {rating7}, {rating8}) are bound to the corresponding Rating_[hash]__c fields returned from Apex.

  4. 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__c object for the AI Scorecard component.

Key Method:

  • getNatterboxAIResults

Objects and Fields:

  • VoiceCall: callreporting__c (Lookup to nbavs__CallReporting__c)

  • Task: nbavs__call_reporting__c (Lookup to nbavs__CallReporting__c)

  • nbavs__NatterboxAI__c: queried via nbavs__Call_Reporting__c lookup. 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__c object. Navigate to Setup > Object Manager > Natterbox AI > Fields & Relationships to find your field names. Remember:

  • Rating prompts generate two fields: Rating_[hash]__c (number) and Reason_[hash]__c (text) with the same hash

  • Free-text prompts generate one field: FreeText_[hash]__c (text)

  • Field labels use RT-, RE-, or FT- 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/Tasknbavs__CallReporting__cnbavs__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 to nbavs__CallReporting__c)

  • Task: nbavs__call_reporting__c (Lookup to nbavs__CallReporting__c)

  • nbavs__NatterboxAI__c: queried via nbavs__Call_Reporting__c lookup

@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]__c fields.

    • backgroundColors: An array containing the colours for each segment of the chart.

    • feedbackArray: An array containing reasoning text from Reason_[hash]__c fields.

  • 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__c object

  • Salesforce 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

  1. Download Visual Studio Code: Go to the Visual Studio Code website and download the version for your operating system.

  2. Install Visual Studio Code: Follow the installation instructions for your OS (Windows .exe, macOS .dmg, or Linux package).

  3. Install Salesforce Extension Pack: Open VS Code, go to Extensions (Ctrl+Shift+X / Cmd+Shift+X), search for "Salesforce Extension Pack", and click Install.

  4. Install Salesforce CLI: Download from the Salesforce CLI download page and follow the installation instructions for your OS.

Step 2: Connecting to Salesforce Sandbox

  1. Open Terminal in VS Code: Go to View > Terminal.

  2. Authenticate to Salesforce:

sfdx force:auth:web:login -r https://test.salesforce.com -a MySandboxAlias
  1. Follow the prompts to log in to your Salesforce sandbox.

  2. Create a Project:

sfdx force:project:create -n AiCallRating
cd AiCallRating

Step 3: Deploying the LWC files

  1. Upload Static Resources to Salesforce:

    1. Log in to your Salesforce sandbox.

    2. Go to Setup > Static Resources.

    3. Click New and upload each static resource one by one: ChartJS, summaryImage, nextStepsImage, nbheader.

    4. Ensure each static resource is set to Cache Control: Public.

  2. Create LWC Component:

sfdx force:lightning:component:create -n aiScorecard -d force-app/main/default/lwc
  1. Add 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).

  2. Push the Component to Salesforce:

sfdx force:source:deploy -p force-app/main/default/lwc -u MySandboxAlias

Pushing Apex classes in VS Code:

sfdx force:source:deploy -p force-app/main/default/classes -u MySandboxAlias

LWC 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.xml

Step 4: Deploying Apex code in Salesforce

  1. Open Developer Console: Log in to your Salesforce sandbox. Click on the gear icon and select Developer Console.

  2. Create Apex Class: Go to File > New > Apex Class. Name it NatterboxAIResultsController. Copy and paste the provided Apex code into the class and save.

  3. 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.

  4. 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

  1. 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

  2. Upload Change Set to Production: Upload the change set to your production environment.

  3. 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.

  4. 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

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__c are org-specific. Each AI Advisor prompt generates fields with a unique hash suffix. Rating prompts create both Rating_[hash]__c and Reason_[hash]__c (same hash). Free-text prompts create FreeText_[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__c object. 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 an RT- and RE- field with the same hash.

  • The CallCenterId value in the test VoiceCall record must be replaced with a valid Call Center ID from your org.