Project 1: Real-time Sentiment Analyzer Web App

7. Project 1: Real-time Sentiment Analyzer Web App

This project will guide you through building a complete, interactive web application for real-time sentiment analysis. You’ll apply the core concepts of Transformers.js, including pipeline initialization, handling user input, and displaying results dynamically, all running entirely in the user’s browser.

7.1. Project Objective and Problem Statement

Objective: Create a web application where users can type or paste text, and the application instantly provides the sentiment (positive, negative, neutral) along with a confidence score.

Problem Statement: Many online sentiment analysis tools require sending data to a server, raising privacy concerns and introducing latency. Our goal is to develop a client-side solution that is fast, privacy-preserving, and easy to use.

7.2. Project Setup

Start with a clean project folder:

mkdir sentiment-app
cd sentiment-app
npm init -y
npm i @huggingface/transformers
npm i -g serve # If you don't have it already

Create index.html and app.js in your sentiment-app directory.

7.3. Step-by-Step Implementation

Step 1: Basic HTML Structure (index.html)

Create index.html with a textarea for input, a button to trigger analysis, and an area to display results.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-time Sentiment Analyzer</title>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Roboto', sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f2f5;
            color: #333;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
        }
        .container {
            background-color: #ffffff;
            padding: 40px;
            border-radius: 12px;
            box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
            width: 100%;
            max-width: 700px;
            text-align: center;
            margin-top: 20px;
        }
        h1 {
            color: #2c3e50;
            margin-bottom: 30px;
            font-weight: 700;
        }
        textarea {
            width: calc(100% - 20px);
            height: 150px;
            margin-bottom: 20px;
            padding: 15px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            font-size: 16px;
            resize: vertical;
            transition: border-color 0.3s ease;
        }
        textarea:focus {
            outline: none;
            border-color: #007bff;
        }
        button {
            background-color: #007bff;
            color: white;
            padding: 14px 30px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 18px;
            font-weight: 700;
            transition: background-color 0.3s ease, transform 0.2s ease;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }
        button:hover:not(:disabled) {
            background-color: #0056b3;
            transform: translateY(-2px);
        }
        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
            transform: translateY(0);
        }
        #output {
            margin-top: 30px;
            padding: 25px;
            border: 1px solid #e0e0e0;
            border-radius: 10px;
            background-color: #fdfdfd;
            text-align: left;
            min-height: 80px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }
        #sentimentResult {
            font-size: 20px;
            font-weight: 700;
            margin: 0;
            padding: 0;
            color: #555;
        }
        .positive { color: #28a745; }
        .negative { color: #dc3545; }
        .neutral { color: #ffc107; }
        #loadingSpinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #007bff;
            border-radius: 50%;
            width: 20px;
            height: 20px;
            animation: spin 1s linear infinite;
            display: none; /* Hidden by default */
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Real-time Sentiment Analyzer</h1>
        <textarea id="inputText" placeholder="Type or paste text here (e.g., 'This product is amazing!')" rows="5"></textarea>
        <button id="analyzeButton">
            <span id="loadingSpinner"></span> Analyze Sentiment
        </button>
        <div id="output">
            <p id="sentimentResult">Waiting for model to load...</p>
        </div>
    </div>

    <script type="module" src="./app.js"></script>
</body>
</html>

Step 2: Initialize the Pipeline (app.js)

In app.js, import pipeline, set up your DOM references, and initialize the sentiment analysis pipeline. We’ll use a distilbert model which is a good balance of speed and accuracy.

// app.js
import { pipeline } from "https://esm.sh/@huggingface/transformers";

document.addEventListener('DOMContentLoaded', async () => {
    const inputText = document.getElementById('inputText');
    const analyzeButton = document.getElementById('analyzeButton');
    const sentimentResult = document.getElementById('sentimentResult');
    const loadingSpinner = document.getElementById('loadingSpinner');

    let sentimentAnalyzer = null; // Declare outside to make it accessible globally

    // --- Loading Model ---
    analyzeButton.disabled = true;
    loadingSpinner.style.display = 'inline-block';
    sentimentResult.textContent = "Loading AI model... this might take a moment (first load).";

    try {
        sentimentAnalyzer = await pipeline(
            'sentiment-analysis',
            'Xenova/distilbert-base-uncased-finetuned-sst-2-english', // A widely used, efficient model
            {
                device: 'webgpu', // Attempt to use WebGPU for max speed
                dtype: 'q4',      // Use 4-bit quantization for smallest size and fastest inference
            }
        );
        sentimentResult.textContent = "Model loaded! Type your text and click 'Analyze'.";
        analyzeButton.disabled = false;
    } catch (error) {
        console.error("Error loading sentiment model:", error);
        sentimentResult.textContent = `Error loading model: ${error.message}. Check console.`;
    } finally {
        loadingSpinner.style.display = 'none';
    }

    // --- Event Listener for Analysis ---
    analyzeButton.addEventListener('click', async () => {
        const text = inputText.value.trim();

        if (!sentimentAnalyzer) {
            sentimentResult.textContent = "Model not loaded yet or failed to load.";
            return;
        }
        if (text === "") {
            sentimentResult.textContent = "Please enter some text to analyze!";
            sentimentResult.className = ''; // Clear previous styling
            return;
        }

        analyzeButton.disabled = true;
        loadingSpinner.style.display = 'inline-block';
        sentimentResult.textContent = "Analyzing...";
        sentimentResult.className = ''; // Clear previous styling

        try {
            const output = await sentimentAnalyzer(text);
            const { label, score } = output[0]; // Extract label and score from the first (and usually only) result

            const confidence = (score * 100).toFixed(2);
            let displayLabel = label;
            let sentimentClass = '';

            // Optional: Implement a neutral threshold
            const neutralThreshold = 0.7; // If confidence is below 70%, consider it neutral for a strong positive/negative label

            if (score < neutralThreshold && (label === 'POSITIVE' || label === 'NEGATIVE')) {
                 displayLabel = 'NEUTRAL';
                 sentimentClass = 'neutral';
            } else if (label === 'POSITIVE') {
                sentimentClass = 'positive';
            } else if (label === 'NEGATIVE') {
                sentimentClass = 'negative';
            } else {
                // For models that might output other labels or if there's an explicit 'NEUTRAL' label
                sentimentClass = 'neutral';
            }


            sentimentResult.innerHTML = `Sentiment: <span class="${sentimentClass}">${displayLabel}</span> (Confidence: ${confidence}%)`;

        } catch (error) {
            console.error("Error during sentiment analysis:", error);
            sentimentResult.textContent = `Analysis failed: ${error.message}`;
            sentimentResult.className = 'negative';
        } finally {
            analyzeButton.disabled = false;
            loadingSpinner.style.display = 'none';
        }
    });
});

Step 3: Serve the Application

Navigate to your sentiment-app directory in the terminal and run:

serve .

Open your browser to the provided local address (e.g., http://localhost:3000).

Step 4: Test and Experiment

  • Type positive sentences like “I absolutely love this new feature!”
  • Type negative sentences like “This is the worst customer service I’ve ever experienced.”
  • Type neutral sentences like “The sky is blue today.” or “I am going to the store.”
  • Observe the sentiment and confidence score. How does the “neutral threshold” (if implemented) affect the results?

7.4. Encouraging Independent Problem-Solving

Now that you have a basic working application, try to implement the following features on your own before looking for solutions:

  1. Live Typing Analysis (Debounce): Instead of requiring a button click, trigger the sentiment analysis as the user types. To prevent too many rapid calls, implement a debounce mechanism. The analysis should only run after the user pauses typing for a short duration (e.g., 500ms).
    • Hint: Use setTimeout and clearTimeout to debounce the inputText’s input event.
  2. Display Multiple Labels/Scores: Some sentiment models might output a score for both POSITIVE and NEGATIVE labels. Modify the output area to show both scores, even if one is chosen as the primary sentiment. For example: “Overall: POSITIVE (98%) - Positive: 98%, Negative: 2%”.
  3. Language Support: The current model is English-specific. Research a multilingual sentiment analysis model compatible with Transformers.js (e.g., Xenova/bert-base-multilingual-cased-sentiment). Allow the user to select a language from a dropdown, and update the model and potentially input processing accordingly. (Note: Fully dynamic multilingual processing is more complex, but you can switch between pre-loaded models based on selection).