Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 8863cf0

Browse files
authored
DEV: Updates to sentiment analysis reports (#1161)
**This PR includes a variety of updates to the Sentiment Analysis report:** - [X] Conditionally showing sentiment reports based on `sentiment_enabled` setting - [X] Sentiment reports should only be visible in sidebar if data is in the reports - [X] Fix infinite loading of posts in drill down - [x] Fix markdown emojis showing not showing as emoji representation - [x] Drill down of posts should have URL - [x] ~~Different doughnut sizing based on post count~~ [reverting and will address in follow-up (see: `/t/146786/47`)] - [X] Hide non-functional export button - [X] Sticky drill down filter nav
1 parent b49d454 commit 8863cf0

File tree

9 files changed

+201
-75
lines changed

9 files changed

+201
-75
lines changed

app/controllers/discourse_ai/sentiment/sentiment_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def posts
3737
SELECT
3838
p.id AS post_id,
3939
p.topic_id,
40-
t.title AS topic_title,
40+
t.fancy_title AS topic_title,
4141
p.cooked as post_cooked,
4242
p.user_id,
4343
p.post_number,

app/models/classification_result.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
class ClassificationResult < ActiveRecord::Base
44
belongs_to :target, polymorphic: true
5+
6+
def self.has_sentiment_classification?
7+
where(classification_type: "sentiment").exists?
8+
end
59
end
610

711
# == Schema Information

assets/javascripts/discourse/components/admin-report-sentiment-analysis.gjs

Lines changed: 107 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,36 @@ import { tracked } from "@glimmer/tracking";
33
import { fn, hash } from "@ember/helper";
44
import { on } from "@ember/modifier";
55
import { action } from "@ember/object";
6+
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
7+
import { service } from "@ember/service";
68
import { modifier } from "ember-modifier";
79
import { and } from "truth-helpers";
810
import DButton from "discourse/components/d-button";
911
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
1012
import PostList from "discourse/components/post-list";
1113
import dIcon from "discourse/helpers/d-icon";
14+
import replaceEmoji from "discourse/helpers/replace-emoji";
1215
import { ajax } from "discourse/lib/ajax";
1316
import { popupAjaxError } from "discourse/lib/ajax-error";
17+
import { getAbsoluteURL } from "discourse/lib/get-url";
18+
import discourseLater from "discourse/lib/later";
19+
import { clipboardCopy } from "discourse/lib/utilities";
1420
import Post from "discourse/models/post";
1521
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
1622
import { i18n } from "discourse-i18n";
23+
import DTooltip from "float-kit/components/d-tooltip";
1724
import DoughnutChart from "discourse/plugins/discourse-ai/discourse/components/doughnut-chart";
1825

1926
export default class AdminReportSentimentAnalysis extends Component {
27+
@service router;
28+
2029
@tracked selectedChart = null;
21-
@tracked posts = null;
30+
@tracked posts = [];
2231
@tracked hasMorePosts = false;
2332
@tracked nextOffset = 0;
2433
@tracked showingSelectedChart = false;
2534
@tracked activeFilter = "all";
35+
@tracked shareIcon = "link";
2636

2737
setActiveFilter = modifier((element) => {
2838
this.clearActiveFilters(element);
@@ -71,32 +81,6 @@ export default class AdminReportSentimentAnalysis extends Component {
7181
}
7282
}
7383

74-
doughnutTitle(data) {
75-
const MAX_TITLE_LENGTH = 18;
76-
const title = data?.title || "";
77-
const score = data?.total_score ? ` (${data.total_score})` : "";
78-
79-
if (title.length + score.length > MAX_TITLE_LENGTH) {
80-
return (
81-
title.substring(0, MAX_TITLE_LENGTH - score.length) + "..." + score
82-
);
83-
}
84-
85-
return title + score;
86-
}
87-
88-
async postRequest() {
89-
return await ajax("/discourse-ai/sentiment/posts", {
90-
data: {
91-
group_by: this.currentGroupFilter,
92-
group_value: this.selectedChart?.title,
93-
start_date: this.args.model.start_date,
94-
end_date: this.args.model.end_date,
95-
offset: this.nextOffset,
96-
},
97-
});
98-
}
99-
10084
get colors() {
10185
return ["#2ecc71", "#95a5a6", "#e74c3c"];
10286
}
@@ -133,10 +117,11 @@ export default class AdminReportSentimentAnalysis extends Component {
133117
}
134118

135119
return this.posts.filter((post) => {
120+
post.topic_title = replaceEmoji(post.topic_title);
121+
136122
if (this.activeFilter === "all") {
137123
return true;
138124
}
139-
140125
return post.sentiment === this.activeFilter;
141126
});
142127
}
@@ -186,13 +171,57 @@ export default class AdminReportSentimentAnalysis extends Component {
186171
];
187172
}
188173

174+
async postRequest() {
175+
return await ajax("/discourse-ai/sentiment/posts", {
176+
data: {
177+
group_by: this.currentGroupFilter,
178+
group_value: this.selectedChart?.title,
179+
start_date: this.args.model.start_date,
180+
end_date: this.args.model.end_date,
181+
offset: this.nextOffset,
182+
},
183+
});
184+
}
185+
186+
@action
187+
async openToChart() {
188+
const queryParams = this.router.currentRoute.queryParams;
189+
if (queryParams.selectedChart) {
190+
this.selectedChart = this.transformedData.find(
191+
(data) => data.title === queryParams.selectedChart
192+
);
193+
194+
if (!this.selectedChart) {
195+
return;
196+
}
197+
this.showingSelectedChart = true;
198+
199+
try {
200+
const response = await this.postRequest();
201+
this.posts = response.posts.map((post) => Post.create(post));
202+
this.hasMorePosts = response.has_more;
203+
this.nextOffset = response.next_offset;
204+
} catch (e) {
205+
popupAjaxError(e);
206+
}
207+
}
208+
}
209+
189210
@action
190211
async showDetails(data) {
191212
if (this.selectedChart === data) {
192213
// Don't do anything if the same chart is clicked again
193214
return;
194215
}
195216

217+
const currentQueryParams = this.router.currentRoute.queryParams;
218+
this.router.transitionTo(this.router.currentRoute.name, {
219+
queryParams: {
220+
...currentQueryParams,
221+
selectedChart: data.title,
222+
},
223+
});
224+
196225
this.selectedChart = data;
197226
this.showingSelectedChart = true;
198227

@@ -217,7 +246,10 @@ export default class AdminReportSentimentAnalysis extends Component {
217246

218247
this.hasMorePosts = response.has_more;
219248
this.nextOffset = response.next_offset;
220-
return response.posts.map((post) => Post.create(post));
249+
250+
const mappedPosts = response.posts.map((post) => Post.create(post));
251+
this.posts.pushObjects(mappedPosts);
252+
return mappedPosts;
221253
} catch (e) {
222254
popupAjaxError(e);
223255
}
@@ -228,9 +260,35 @@ export default class AdminReportSentimentAnalysis extends Component {
228260
this.showingSelectedChart = false;
229261
this.selectedChart = null;
230262
this.activeFilter = "all";
263+
this.posts = [];
264+
265+
const currentQueryParams = this.router.currentRoute.queryParams;
266+
this.router.transitionTo(this.router.currentRoute.name, {
267+
queryParams: {
268+
...currentQueryParams,
269+
selectedChart: null,
270+
},
271+
});
272+
}
273+
274+
@action
275+
shareChart() {
276+
const url = this.router.currentURL;
277+
if (!url) {
278+
return;
279+
}
280+
281+
clipboardCopy(getAbsoluteURL(url));
282+
this.shareIcon = "check";
283+
284+
discourseLater(() => {
285+
this.shareIcon = "link";
286+
}, 2000);
231287
}
232288

233289
<template>
290+
<span {{didInsert this.openToChart}}></span>
291+
234292
{{#unless this.showingSelectedChart}}
235293
<div class="admin-report-sentiment-analysis">
236294
{{#each this.transformedData as |data|}}
@@ -252,6 +310,7 @@ export default class AdminReportSentimentAnalysis extends Component {
252310
@data={{data.scores}}
253311
@totalScore={{data.total_score}}
254312
@doughnutTitle={{data.title}}
313+
@displayLegend={{true}}
255314
/>
256315
</div>
257316
{{/each}}
@@ -260,20 +319,33 @@ export default class AdminReportSentimentAnalysis extends Component {
260319

261320
{{#if (and this.selectedChart this.showingSelectedChart)}}
262321
<div class="admin-report-sentiment-analysis__selected-chart">
263-
<DButton
264-
@label="back_button"
265-
@icon="chevron-left"
266-
class="btn-flat"
267-
@action={{this.backToAllCharts}}
268-
/>
322+
<div class="admin-report-sentiment-analysis__selected-chart-actions">
323+
<DButton
324+
@label="back_button"
325+
@icon="chevron-left"
326+
class="btn-flat"
327+
@action={{this.backToAllCharts}}
328+
/>
329+
330+
<DTooltip
331+
class="share btn-flat"
332+
@icon={{this.shareIcon}}
333+
{{on "click" this.shareChart}}
334+
@content={{i18n
335+
"discourse_ai.sentiments.sentiment_analysis.share_chart"
336+
}}
337+
/>
338+
</div>
269339

270340
<DoughnutChart
271341
@labels={{@model.labels}}
272342
@colors={{this.colors}}
273343
@data={{this.selectedChart.scores}}
274344
@totalScore={{this.selectedChart.total_score}}
275345
@doughnutTitle={{this.selectedChart.title}}
346+
@displayLegend={{true}}
276347
/>
348+
277349
</div>
278350
<div class="admin-report-sentiment-analysis-details">
279351
<HorizontalOverflowNav

assets/javascripts/discourse/components/doughnut-chart.gjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
23
import Chart from "admin/components/chart";
34

45
export default class DoughnutChart extends Component {
6+
@tracked canvasSize = null;
7+
58
get config() {
69
const totalScore = this.args.totalScore || "";
710

@@ -13,14 +16,18 @@ export default class DoughnutChart extends Component {
1316
{
1417
data: this.args.data,
1518
backgroundColor: this.args.colors,
19+
cutout: "50%",
20+
radius: 100,
1621
},
1722
],
1823
},
1924
options: {
2025
responsive: true,
26+
maintainAspectRatio: false,
2127
plugins: {
2228
legend: {
23-
position: this.args.legendPosition || "bottom",
29+
display: this.args.displayLegend || false,
30+
position: "bottom",
2431
},
2532
},
2633
},

assets/javascripts/initializers/admin-reports.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ export default {
2222
"sentiment_analysis",
2323
AdminReportSentimentAnalysis
2424
);
25+
26+
api.registerValueTransformer(
27+
"admin-reports-show-query-params",
28+
({ value }) => {
29+
return [...value, "selectedChart"];
30+
}
31+
);
2532
});
2633
},
2734
};
Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { apiInitializer } from "discourse/lib/api";
22

33
export default apiInitializer("1.15.0", (api) => {
4-
const settings = api.container.lookup("service:site-settings");
4+
const currentUser = api.getCurrentUser();
55

6-
if (settings.ai_sentiment_enabled) {
7-
api.addAdminSidebarSectionLink("reports", {
8-
name: "sentiment_overview",
9-
route: "admin.dashboardSentiment",
10-
label: "discourse_ai.sentiments.sidebar.overview",
11-
icon: "chart-column",
12-
});
13-
api.addAdminSidebarSectionLink("reports", {
14-
name: "sentiment_analysis",
15-
route: "adminReports.show",
16-
routeModels: ["sentiment_analysis"],
17-
label: "discourse_ai.sentiments.sidebar.analysis",
18-
icon: "chart-pie",
19-
});
6+
if (
7+
!currentUser ||
8+
!currentUser.admin ||
9+
!currentUser.can_see_sentiment_reports
10+
) {
11+
return;
2012
}
13+
14+
api.addAdminSidebarSectionLink("reports", {
15+
name: "sentiment_overview",
16+
route: "admin.dashboardSentiment",
17+
label: "discourse_ai.sentiments.sidebar.overview",
18+
icon: "chart-column",
19+
});
20+
api.addAdminSidebarSectionLink("reports", {
21+
name: "sentiment_analysis",
22+
route: "adminReports.show",
23+
routeModels: ["sentiment_analysis"],
24+
label: "discourse_ai.sentiments.sidebar.analysis",
25+
icon: "chart-pie",
26+
});
2127
});

0 commit comments

Comments
 (0)