---
title: AI Sentiment Analysis for Open-Ended Questions
product: Dashboard
description: Learn how to analyze open-ended survey responses with AI-powered sentiment analysis. This demo combines a client-side SurveyJS Dashboard that visualizes the data with a backend that processes responses and returns sentiment scores.
framework: React
source: https://surveyjs.io/dashboard/examples/analyze-open-ended-survey-responses-ai-sentiment-analysis/reactjs
index: https://surveyjs.io/dashboard/examples/overview.md
---

# AI Sentiment Analysis for Open-Ended Questions (React)

Text-based responses, especially open-ended feedback, can be difficult to analyze in their raw form. To make them easier to interpret, you can apply sentiment analysis to assign a score that reflects how positive or negative the overall tone is. You can also generate word clouds to highlight what respondents are most satisfied with and what they complain about most often. This demo showcases AI-powered text analysis applied to hotel reviews.

## Implementation

Sentiment analysis in this example has two parts:

- Client side       
A SurveyJS Dashboard that requests sentiment statistics from the server and visualizes them.

- Server side           
A backend that stores responses, calculates sentiment scores, and returns the results to the client.

In this demo, responses are submitted on the client side. The AI model that performs sentiment analysis runs on the SurveyJS website, and the following C# code is used to handle server requests:

<details>
    <summary>View Backend Code</summary>

```csharp
private Dictionary<string, int> GetSentimentEntities(AnalyzeSentimentResultCollection reviews, TextSentiment sentiment) {
    Dictionary<string, int> complaints = new Dictionary<string, int>();
    foreach(AnalyzeSentimentResult review in reviews) {
        foreach(SentenceSentiment sentence in review.DocumentSentiment.Sentences) {
            foreach(SentenceOpinion opinion in sentence.Opinions) {
                if(opinion.Target.Sentiment == sentiment) {
                    int value = 0;
                    complaints.TryGetValue(opinion.Target.Text, out value);
                    complaints[opinion.Target.Text] = value + 1;
                }
            }
        }
    }
    return complaints;
}

[HttpPost]
[ActionName("sentiment")]
public async Task<IHttpActionResult> GetSentiment([FromBody] RequestTextsDto request) {
    List<string> batchedDocuments = request.texts;

    var endpoint = "your-endpoint";
    var credential = new AzureKeyCredential("your-Azure-credential");
    TextAnalyticsClient client = new TextAnalyticsClient(new Uri(endpoint), credential);
    AnalyzeSentimentOptions options = new AnalyzeSentimentOptions() { IncludeOpinionMining = true };
    Response<AnalyzeSentimentResultCollection> response = await client.AnalyzeSentimentBatchAsync(batchedDocuments, options: options);
    AnalyzeSentimentResultCollection reviews = response.Value;

    Dictionary<string, int> complaints = GetSentimentEntities(reviews, TextSentiment.Negative);
    Dictionary<string, int> praises = GetSentimentEntities(reviews, TextSentiment.Positive);

    float averagePositive = reviews.Aggregate((float)0, (a, b) => a + (float)b.DocumentSentiment.ConfidenceScores.Positive) / reviews.Count;
    float averageNegative = reviews.Aggregate((float)0, (a, b) => a - (float)b.DocumentSentiment.ConfidenceScores.Negative) / reviews.Count;
    float value = reviews.Aggregate((float)0, (a, b) => a + (float)b.DocumentSentiment.ConfidenceScores.Positive - (float)b.DocumentSentiment.ConfidenceScores.Negative) / reviews.Count;
    switch(request.visualizer) {
        case "complaints":
            return Ok(complaints);
        case "praises":
            return Ok(praises);
        case "polarity":
            return Ok(new { value = averagePositive });
    }
    return Ok(new { praises = praises, complaints = complaints, value = value, averagePositive = averagePositive, averageNegative = averageNegative });
}
```

</details>

## Files

### `public/index.html`

```html
<div id="loadingIndicator" class="data-loading-indicator-panel">
  <div class="data-loading-indicator">
    <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
      <g clip-path="url(#clip0_17928_11482)">
        <path d="M32 64C14.36 64 0 49.65 0 32C0 14.35 14.36 0 32 0C49.64 0 64 14.35 64 32C64 49.65 49.64 64 32 64ZM32 4C16.56 4 4 16.56 4 32C4 47.44 16.56 60 32 60C47.44 60 60 47.44 60 32C60 16.56 47.44 4 32 4Z" fill="#E5E5E5" />
        <path d="M53.2101 55.2104C52.7001 55.2104 52.1901 55.0104 51.8001 54.6204C51.0201 53.8404 51.0201 52.5704 51.8001 51.7904C57.0901 46.5004 60.0001 39.4704 60.0001 31.9904C60.0001 24.5104 57.0901 17.4804 51.8001 12.1904C51.0201 11.4104 51.0201 10.1404 51.8001 9.36039C52.5801 8.58039 53.8501 8.58039 54.6301 9.36039C60.6701 15.4004 64.0001 23.4404 64.0001 31.9904C64.0001 40.5404 60.6701 48.5704 54.6301 54.6204C54.2401 55.0104 53.7301 55.2104 53.2201 55.2104H53.2101Z" fill="#19B394" />
      </g>
      <defs>
        <clipPath id="clip0_17928_11482">
          <rect width="64" height="64" fill="white" />
        </clipPath>
      </defs>
    </svg>
  </div>
</div>
<div id="surveyDashboardComponent"></div>
```

### `src/surveydata.js`

```js
export const dataFromServer = [
  {
    review: "The food and service were unacceptable, but the concierge were nice. After talking to them about the"
      + " quality of the food and the process to get room service they refunded the money we spent at the"
      + " restaurant and gave us a voucher for nearby restaurants.",
  },
  {
    review: "The rooms were beautiful. The AC was good and quiet, which was key for us as outside it was 100F and"
      + " our baby was getting uncomfortable because of the heat. The breakfast was good too with good"
      + " options and good servicing times. The thing we didn't like was that the toilet in our bathroom was"
      + " smelly. It could have been that the toilet was not cleaned before we arrived. Either way it was"
      + " very uncomfortable. Once we notified the staff, they came and cleaned it and left candles.",
  },
  {
    review: "Nice rooms! I had a great unobstructed view of a campus but bathrooms were old and the"
      + " toilet was dirty when we arrived. It was close to bus stops and groceries stores. If you want to"
      + " be close to campus I will recommend it, otherwise, might be better to stay in a cleaner one."
  },
  {
    review: "We stayed for three nights and overall it was pleasant. The location is perfect—right next to the"
      + " metro and several coffee shops. The staff were friendly and helpful. The downside was the thin walls;"
      + " we could hear our neighbors talking late into the night which made it hard to sleep.",
  },
  {
    review: "Absolutely loved the rooftop pool! The views of the city skyline at sunset were amazing."
      + " The bar service was quick and the cocktails were delicious. Rooms were spacious and modern,"
      + " though the Wi-Fi kept disconnecting which was frustrating since I needed to work remotely.",
  },
  {
    review: "The hotel looks great in pictures but in reality it felt outdated. Carpets were stained and the"
      + " elevators took forever to arrive. On the plus side, the bed was very comfortable and housekeeping"
      + " did a good job refreshing our room daily.",
  },
  {
    review: "Our stay was disappointing. The check-in took almost an hour because the front desk lost our reservation."
      + " The staff seemed overwhelmed and unorganized. When we finally got our room, the sheets had stains and"
      + " the minibar was half empty. Wouldn't recommend.",
  },
  {
    review: "Terrible experience. The air conditioning didn't work during a heatwave and the staff only offered us"
      + " a small fan. The room smelled of mold, and the shower drain was clogged. We cut our trip short and moved"
      + " to another hotel.",
  },
  {
    review: "I was here for a business trip and the facilities were not up to standard. The Wi-Fi kept dropping in the"
      + " middle of meetings, and the conference room chairs were broken. The catering was bland and late every day."
      + " Not a professional environment at all.",
  },
  {
    review: "Very noisy at night due to a nightclub next door. Even with earplugs, it was impossible to sleep before 3am."
      + " The breakfast buffet was cold and tasteless, and the staff didn't seem to care when we complained. Definitely"
      + " not worth the price we paid.",
  }
];
```

### `src/SurveyDashboardComponent.jsx`

```js
import React from "react";
import { Model } from "survey-core";
import { Dashboard } from "survey-analytics";
import "survey-analytics/survey.analytics.css";
import "./index.css";
import { json } from "./json";
import "survey-core/survey-core.min.css";
import { dataFromServer } from "./surveydata";

class SurveyDashboardComponent extends React.Component {
    componentDidMount() {
        const survey = new Model(json);
        // Imitate an asynchronous call that loads data from a server
        setTimeout(() => {
            const dashboard = new Dashboard({
                questions: survey.getAllQuestions(),
                data: dataFromServer,
                allowHideQuestions: false,
                allowChangeVisualizerType: false,
                showToolbar: false,
                items: [
                    {
                        name: "polarity",
                        type: "gauge"
                    },
                    {
                        name: "review",
                        type: "text"
                    },
                    "praises",
                    "complaints"
                ]
            });
            
            
                const statisticsCache = {
                review: {
                    data: dataFromServer.map(di => [di.review]).filter(r => !!r),
                    values: [],
                    series: [""]
                }
            };
        
            function getSentimentDashboardData({ visualizer }) {
                return new Promise((resolve, reject) => {
                    const datasetName = visualizer.name;
                    if (statisticsCache[datasetName]) {
                        resolve(statisticsCache[datasetName]);
                        return;
                    }
                    fetch("/api/ai/sentiment", {
                        method: "POST",
                        headers: {
                            'Accept': 'application/json, text/plain, */*',
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            texts: dataFromServer.map(item => item.review).filter(t => !!t),
                            visualizer: datasetName
                        }),
                    })
                    .then(response =>
                        response.json().then(data => {
                            statisticsCache[datasetName] = data;
                            resolve(data);
                        })
                    )
                    .catch(reason => reject(reason));
                });
            }
        
        
            dashboard.dataProvider.data = getSentimentDashboardData;
            dashboard.showToolbar = false;
            dashboard.getVisualizer("review").setVisualizer("text");
            dashboard.getVisualizer("polarity").setVisualizer("gauge");
        
            
            document.getElementById("loadingIndicator").style.display = "none";
            dashboard.render("surveyDashboardContainer");
            
        }, 1000);
    }
    render() {
        return React.createElement("div", { id: "surveyDashboardContainer" });
    }
}

export default SurveyDashboardComponent;
```

### `src/index.css`

```css
/* You can add your custom CSS here. */
.data-loading-indicator-panel {
    width: 100%;
    height: 400px;
}
.data-loading-indicator {
    position: relative;
    width: 64px;
    height: 64px;
    left: calc((100% - 64px)/ 2);
    top: calc((100% - 64px)/ 2);
    animation: data-loading-indicator-spinner 1s infinite linear;
}
@keyframes data-loading-indicator-spinner {
    from {
        transform: rotate(0deg);
    }

    to {
        transform: rotate(359deg);
    }
}
```

### `src/index.js`

```js
import React from "react";
import { createRoot } from "react-dom/client";
import SurveyDashboardComponent from "./SurveyDashboardComponent";

const root = createRoot(document.getElementById("surveyDashboardComponent"));
root.render(<SurveyDashboardComponent />);
```

### `src/json.js`

```js
export const json = {
  "elements": [
    {
      "type": "rating",
      "name": "polarity",
      "rateMin": -1,
      "rateMax": 1,
      "title": "Overall sentiment score"
    },
    {
      "type": "comment",
      "name": "review",
      "title": "What did you think about your stay at our hotel?"
    },
    {
      "type": "comment",
      "name": "praises",
      "title": "Things guests liked most"
    },
    {
      "type": "comment",
      "name": "complaints",
      "title": "Things guests complained about most"
    }
  ]
};
```

### `package.json`

```json
{
  "dependencies": {
    "react": "latest",
    "react-dom": "latest",
    "survey-core": "latest",
    "chart.js": "4.5.1",
    "survey-analytics": "latest"
  },
  "devDependencies": {
    "react-scripts": "latest"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ]
}
```

## Other Frameworks

- [Angular](https://surveyjs.io/dashboard/examples/analyze-open-ended-survey-responses-ai-sentiment-analysis/angular.md)
- [Vue 3](https://surveyjs.io/dashboard/examples/analyze-open-ended-survey-responses-ai-sentiment-analysis/vue3js.md)
- [jQuery](https://surveyjs.io/dashboard/examples/analyze-open-ended-survey-responses-ai-sentiment-analysis/jquery.md)
- [Vanilla JS](https://surveyjs.io/dashboard/examples/analyze-open-ended-survey-responses-ai-sentiment-analysis/vanillajs.md)
