--- 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. ![](https://cdn.document360.io/c81c490c-daaa-4ebd-a3c3-1a2dee8b7617/Images/Documentation/image(499).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. ![](https://cdn.document360.io/c81c490c-daaa-4ebd-a3c3-1a2dee8b7617/Images/Documentation/image(500).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 ``` ### 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 ```xml
  •    {rating7}        Problem-Solving
  •    {rating8}        Patience
  • ``` #### Explanation of changes 1. **Left Column Adjustments**: Add a new
  • 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    58.0    true            lightning__RecordPage                                            VoiceCall                Task                         ``` ### 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 ```css .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) ```java public with sharing class NatterboxAIResultsController {    @AuraEnabled(cacheable=true)    public static List getNatterboxAIResults(List recordIds) {        try {            // Determine the record type based on the prefix            String recordPrefix = String.valueOf(recordIds[0]).substring(0, 3);            List callReportingIds = new List();            if (recordPrefix == '0LQ') {                // Handle VoiceCall records                List 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 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 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/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: ```java List 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 ```java @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 results = NatterboxAIResultsController.getNatterboxAIResults(new List { 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 results = NatterboxAIResultsController.getNatterboxAIResults(new List { 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 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](https://code.visualstudio.com/) 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](https://developer.salesforce.com/tools/sfdxcli) 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: ```bash 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: ```bash 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:** ```bash 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:** ```bash sfdx force:source:deploy -p force-app/main/default/lwc -u MySandboxAlias ``` **Pushing Apex classes in VS Code:** ```bash sfdx force:source:deploy -p force-app/main/default/classes -u MySandboxAlias ``` **LWC Folder Structure with Apex classes included:** ```bash 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 - **Visual Studio Code Documentation**: [Getting Started with Visual Studio Code](https://code.visualstudio.com/docs) - **Salesforce CLI Documentation**: [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm) - **Salesforce Extensions for VS Code**: [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/) #### 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. ## ## Related - [Pushing Prompt Results to Tasks](/pushing-prompt-results-to-tasks.md) - [How to make use of AI rated calls](/how-to-make-use-of-ai-rated-calls.md) - [Setting up Prompts](/ai-prompts.md) - [AI Advisor Prompt Template Library](/prompt-library.md)