discourse-ai/assets/javascripts/discourse/components/admin-report-sentiment-anal...

321 lines
9.0 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 } from "@ember/object";
import { modifier } from "ember-modifier";
import { and } from "truth-helpers";
import DButton from "discourse/components/d-button";
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
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;
@tracked hasMorePosts = false;
@tracked nextOffset = 0;
@tracked showingSelectedChart = false;
@tracked activeFilter = "all";
setActiveFilter = modifier((element) => {
this.clearActiveFilters(element);
element
.querySelector(`li[data-filter-type="${this.activeFilter}"] button`)
.classList.add("active");
});
clearActiveFilters(element) {
const filterButtons = element.querySelectorAll("li button");
for (let button of filterButtons) {
button.classList.remove("active");
}
}
calculateNeutralScore(data) {
return data.total_count - (data.positive_count + data.negative_count);
}
sentimentMapping(sentiment) {
switch (sentiment) {
case "positive":
return {
id: "positive",
text: i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.positive"
),
icon: "face-smile",
};
case "neutral":
return {
id: "neutral",
text: i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.neutral"
),
icon: "face-meh",
};
case "negative":
return {
id: "negative",
text: i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.negative"
),
icon: "face-angry",
};
}
}
doughnutTitle(data) {
const MAX_TITLE_LENGTH = 18;
const title = data?.title || "";
const score = data?.total_score ? ` (${data.total_score})` : "";
if (title.length + score.length > MAX_TITLE_LENGTH) {
return (
title.substring(0, MAX_TITLE_LENGTH - score.length) + "..." + score
);
}
return title + score;
}
async postRequest() {
return await ajax("/discourse-ai/sentiment/posts", {
data: {
group_by: this.currentGroupFilter,
group_value: this.selectedChart?.title,
start_date: this.args.model.start_date,
end_date: this.args.model.end_date,
offset: this.nextOffset,
},
});
}
get colors() {
return ["#2ecc71", "#95a5a6", "#e74c3c"];
}
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,
};
});
}
get filteredPosts() {
if (!this.posts || !this.posts.length) {
return [];
}
return this.posts.filter((post) => {
if (this.activeFilter === "all") {
return true;
}
return post.sentiment === this.activeFilter;
});
}
get postFilters() {
return [
{
id: "all",
text: `${i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.all"
)} (${this.selectedChart.total_score})`,
icon: "bars-staggered",
action: () => {
this.activeFilter = "all";
},
},
{
id: "positive",
text: `${i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.positive"
)} (${this.selectedChart.scores[0]})`,
icon: "face-smile",
action: () => {
this.activeFilter = "positive";
},
},
{
id: "neutral",
text: `${i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.neutral"
)} (${this.selectedChart.scores[1]})`,
icon: "face-meh",
action: () => {
this.activeFilter = "neutral";
},
},
{
id: "negative",
text: `${i18n(
"discourse_ai.sentiments.sentiment_analysis.filter_types.negative"
)} (${this.selectedChart.scores[2]})`,
icon: "face-angry",
action: () => {
this.activeFilter = "negative";
},
},
];
}
@action
async showDetails(data) {
if (this.selectedChart === data) {
// Don't do anything if the same chart is clicked again
return;
}
this.selectedChart = data;
this.showingSelectedChart = true;
try {
const response = await this.postRequest();
this.posts = response.posts.map((post) => Post.create(post));
this.hasMorePosts = response.has_more;
this.nextOffset = response.next_offset;
} catch (e) {
popupAjaxError(e);
}
}
@action
async fetchMorePosts() {
if (!this.hasMorePosts || this.selectedChart === null) {
return [];
}
try {
const response = await this.postRequest();
this.hasMorePosts = response.has_more;
this.nextOffset = response.next_offset;
return response.posts.map((post) => Post.create(post));
} catch (e) {
popupAjaxError(e);
}
}
@action
backToAllCharts() {
this.showingSelectedChart = false;
this.selectedChart = null;
this.activeFilter = "all";
}
<template>
{{#unless this.showingSelectedChart}}
<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>
{{/unless}}
{{#if (and this.selectedChart this.showingSelectedChart)}}
<div class="admin-report-sentiment-analysis__selected-chart">
<DButton
@label="back_button"
@icon="chevron-left"
class="btn-flat"
@action={{this.backToAllCharts}}
/>
<h3 class="admin-report-sentiment-analysis-details__title">
{{this.selectedChart.title}}
</h3>
<DoughnutChart
@labels={{@model.labels}}
@colors={{this.colors}}
@data={{this.selectedChart.scores}}
@doughnutTitle={{this.doughnutTitle this.selectedChart}}
/>
</div>
<div class="admin-report-sentiment-analysis-details">
<HorizontalOverflowNav
{{this.setActiveFilter}}
class="admin-report-sentiment-analysis-details__filters"
>
{{#each this.postFilters as |filter|}}
<li data-filter-type={{filter.id}}>
<DButton
@icon={{filter.icon}}
@translatedLabel={{filter.text}}
@action={{filter.action}}
class="btn-transparent"
/>
</li>
{{/each}}
</HorizontalOverflowNav>
<PostList
@posts={{this.filteredPosts}}
@urlPath="url"
@idPath="post_id"
@titlePath="topic_title"
@usernamePath="username"
@fetchMorePosts={{this.fetchMorePosts}}
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>
}