mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-11-26 02:51:04 +00:00
## 🔍 Overview This update adds a new report page at `admin/reports/sentiment_analysis` where admins can see a sentiment analysis report for the forum grouped by either category or tags. ## ➕ More details The report can breakdown either category or tags into positive/negative/neutral sentiments based on the grouping (category/tag). Clicking on the doughnut visualization will bring up a post list of all the posts that were involved in that classification with further sentiment classifications by post. The report can additionally be sorted in alphabetical order or by size, as well as be filtered by either category/tag based on the grouping. ## 👨🏽💻 Technical Details The new admin report is registered via the pluginAPi with `api.registerReportModeComponent` to register the custom sentiment doughnut report. However, when each doughnut visualization is clicked, a new endpoint found at: `/discourse-ai/sentiment/posts` is fetched to showcase posts classified by sentiments based on the respective params. ## 📸 Screenshots 
185 lines
5.3 KiB
Plaintext
185 lines
5.3 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { fn, hash } from "@ember/helper";
|
|
import { on } from "@ember/modifier";
|
|
import { action, get } from "@ember/object";
|
|
import PostList from "discourse/components/post-list";
|
|
import dIcon from "discourse/helpers/d-icon";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import Post from "discourse/models/post";
|
|
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
|
|
import { i18n } from "discourse-i18n";
|
|
import DoughnutChart from "discourse/plugins/discourse-ai/discourse/components/doughnut-chart";
|
|
|
|
export default class AdminReportSentimentAnalysis extends Component {
|
|
@tracked selectedChart = null;
|
|
@tracked posts = null;
|
|
|
|
get colors() {
|
|
return ["#2ecc71", "#95a5a6", "#e74c3c"];
|
|
}
|
|
|
|
calculateNeutralScore(data) {
|
|
return data.total_count - (data.positive_count + data.negative_count);
|
|
}
|
|
|
|
get currentGroupFilter() {
|
|
return this.args.model.available_filters.find(
|
|
(filter) => filter.id === "group_by"
|
|
).default;
|
|
}
|
|
|
|
get currentSortFilter() {
|
|
return this.args.model.available_filters.find(
|
|
(filter) => filter.id === "sort_by"
|
|
).default;
|
|
}
|
|
|
|
get transformedData() {
|
|
return this.args.model.data.map((data) => {
|
|
return {
|
|
title: data.category_name || data.tag_name,
|
|
scores: [
|
|
data.positive_count,
|
|
this.calculateNeutralScore(data),
|
|
data.negative_count,
|
|
],
|
|
total_score: data.total_count,
|
|
};
|
|
});
|
|
}
|
|
|
|
@action
|
|
async showDetails(data) {
|
|
this.selectedChart = data;
|
|
try {
|
|
const posts = await ajax(`/discourse-ai/sentiment/posts`, {
|
|
data: {
|
|
group_by: this.currentGroupFilter,
|
|
group_value: data.title,
|
|
start_date: this.args.model.start_date,
|
|
end_date: this.args.model.end_date,
|
|
},
|
|
});
|
|
|
|
this.posts = posts.map((post) => Post.create(post));
|
|
} catch (e) {
|
|
popupAjaxError(e);
|
|
}
|
|
}
|
|
|
|
sentimentMapping(sentiment) {
|
|
switch (sentiment) {
|
|
case "positive":
|
|
return {
|
|
id: "positive",
|
|
text: i18n(
|
|
"discourse_ai.sentiments.sentiment_analysis.score_types.positive"
|
|
),
|
|
icon: "face-smile",
|
|
};
|
|
case "neutral":
|
|
return {
|
|
id: "neutral",
|
|
text: i18n(
|
|
"discourse_ai.sentiments.sentiment_analysis.score_types.neutral"
|
|
),
|
|
icon: "face-meh",
|
|
};
|
|
case "negative":
|
|
return {
|
|
id: "negative",
|
|
text: i18n(
|
|
"discourse_ai.sentiments.sentiment_analysis.score_types.negative"
|
|
),
|
|
icon: "face-angry",
|
|
};
|
|
}
|
|
}
|
|
|
|
doughnutTitle(data) {
|
|
if (data?.total_score) {
|
|
return `${data.title} (${data.total_score})`;
|
|
} else {
|
|
return data.title;
|
|
}
|
|
}
|
|
|
|
<template>
|
|
<div class="admin-report-sentiment-analysis">
|
|
{{#each this.transformedData as |data|}}
|
|
<div
|
|
class="admin-report-sentiment-analysis__chart-wrapper"
|
|
role="button"
|
|
{{on "click" (fn this.showDetails data)}}
|
|
{{closeOnClickOutside
|
|
(fn (mut this.selectedChart) null)
|
|
(hash
|
|
targetSelector=".admin-report-sentiment-analysis-details"
|
|
secondaryTargetSelector=".admin-report-sentiment-analysis"
|
|
)
|
|
}}
|
|
>
|
|
<DoughnutChart
|
|
@labels={{@model.labels}}
|
|
@colors={{this.colors}}
|
|
@data={{data.scores}}
|
|
@doughnutTitle={{this.doughnutTitle data}}
|
|
/>
|
|
</div>
|
|
{{/each}}
|
|
</div>
|
|
|
|
{{#if this.selectedChart}}
|
|
<div class="admin-report-sentiment-analysis-details">
|
|
<h3 class="admin-report-sentiment-analysis-details__title">
|
|
{{this.selectedChart.title}}
|
|
</h3>
|
|
|
|
<ul class="admin-report-sentiment-analysis-details__scores">
|
|
<li>
|
|
{{dIcon "face-smile" style="color: #2ecc71"}}
|
|
{{i18n
|
|
"discourse_ai.sentiments.sentiment_analysis.score_types.positive"
|
|
}}:
|
|
{{get this.selectedChart.scores 0}}</li>
|
|
<li>
|
|
{{dIcon "face-meh"}}
|
|
{{i18n
|
|
"discourse_ai.sentiments.sentiment_analysis.score_types.neutral"
|
|
}}:
|
|
{{get this.selectedChart.scores 1}}</li>
|
|
<li>
|
|
{{dIcon "face-angry"}}
|
|
{{i18n
|
|
"discourse_ai.sentiments.sentiment_analysis.score_types.negative"
|
|
}}:
|
|
{{get this.selectedChart.scores 2}}</li>
|
|
</ul>
|
|
|
|
<PostList
|
|
@posts={{this.posts}}
|
|
@urlPath="url"
|
|
@idPath="post_id"
|
|
@titlePath="topic_title"
|
|
@usernamePath="username"
|
|
class="admin-report-sentiment-analysis-details__post-list"
|
|
>
|
|
<:abovePostItemExcerpt as |post|>
|
|
{{#let (this.sentimentMapping post.sentiment) as |sentiment|}}
|
|
<span
|
|
class="admin-report-sentiment-analysis-details__post-score"
|
|
data-sentiment-score={{sentiment.id}}
|
|
>
|
|
{{dIcon sentiment.icon}}
|
|
{{sentiment.text}}
|
|
</span>
|
|
{{/let}}
|
|
</:abovePostItemExcerpt>
|
|
</PostList>
|
|
</div>
|
|
{{/if}}
|
|
</template>
|
|
}
|