From 8863cf0c86140065978d33ee9dca421fc8670fb5 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 5 Mar 2025 13:53:56 -0800 Subject: [PATCH] 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 --- .../sentiment/sentiment_controller.rb | 2 +- app/models/classification_result.rb | 4 + .../admin-report-sentiment-analysis.gjs | 142 +++++++++++++----- .../discourse/components/doughnut-chart.gjs | 9 +- .../javascripts/initializers/admin-reports.js | 7 + .../initializers/ai-sentiment-admin-nav.js | 36 +++-- .../modules/sentiment/common/dashboard.scss | 66 +++++--- config/locales/client.en.yml | 1 + lib/sentiment/entry_point.rb | 15 +- 9 files changed, 204 insertions(+), 78 deletions(-) diff --git a/app/controllers/discourse_ai/sentiment/sentiment_controller.rb b/app/controllers/discourse_ai/sentiment/sentiment_controller.rb index 57fa4d33..220488ec 100644 --- a/app/controllers/discourse_ai/sentiment/sentiment_controller.rb +++ b/app/controllers/discourse_ai/sentiment/sentiment_controller.rb @@ -37,7 +37,7 @@ module DiscourseAi SELECT p.id AS post_id, p.topic_id, - t.title AS topic_title, + t.fancy_title AS topic_title, p.cooked as post_cooked, p.user_id, p.post_number, diff --git a/app/models/classification_result.rb b/app/models/classification_result.rb index 8da1c16d..1d3f7b12 100644 --- a/app/models/classification_result.rb +++ b/app/models/classification_result.rb @@ -2,6 +2,10 @@ class ClassificationResult < ActiveRecord::Base belongs_to :target, polymorphic: true + + def self.has_sentiment_classification? + where(classification_type: "sentiment").exists? + end end # == Schema Information diff --git a/assets/javascripts/discourse/components/admin-report-sentiment-analysis.gjs b/assets/javascripts/discourse/components/admin-report-sentiment-analysis.gjs index a1ca1e6f..f49745eb 100644 --- a/assets/javascripts/discourse/components/admin-report-sentiment-analysis.gjs +++ b/assets/javascripts/discourse/components/admin-report-sentiment-analysis.gjs @@ -3,26 +3,36 @@ import { tracked } from "@glimmer/tracking"; import { fn, hash } from "@ember/helper"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import { service } from "@ember/service"; 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 replaceEmoji from "discourse/helpers/replace-emoji"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { getAbsoluteURL } from "discourse/lib/get-url"; +import discourseLater from "discourse/lib/later"; +import { clipboardCopy } from "discourse/lib/utilities"; import Post from "discourse/models/post"; import closeOnClickOutside from "discourse/modifiers/close-on-click-outside"; import { i18n } from "discourse-i18n"; +import DTooltip from "float-kit/components/d-tooltip"; import DoughnutChart from "discourse/plugins/discourse-ai/discourse/components/doughnut-chart"; export default class AdminReportSentimentAnalysis extends Component { + @service router; + @tracked selectedChart = null; - @tracked posts = null; + @tracked posts = []; @tracked hasMorePosts = false; @tracked nextOffset = 0; @tracked showingSelectedChart = false; @tracked activeFilter = "all"; + @tracked shareIcon = "link"; setActiveFilter = modifier((element) => { this.clearActiveFilters(element); @@ -71,32 +81,6 @@ export default class AdminReportSentimentAnalysis extends Component { } } - 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"]; } @@ -133,10 +117,11 @@ export default class AdminReportSentimentAnalysis extends Component { } return this.posts.filter((post) => { + post.topic_title = replaceEmoji(post.topic_title); + if (this.activeFilter === "all") { return true; } - return post.sentiment === this.activeFilter; }); } @@ -186,6 +171,42 @@ export default class AdminReportSentimentAnalysis extends Component { ]; } + 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, + }, + }); + } + + @action + async openToChart() { + const queryParams = this.router.currentRoute.queryParams; + if (queryParams.selectedChart) { + this.selectedChart = this.transformedData.find( + (data) => data.title === queryParams.selectedChart + ); + + if (!this.selectedChart) { + return; + } + 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 showDetails(data) { if (this.selectedChart === data) { @@ -193,6 +214,14 @@ export default class AdminReportSentimentAnalysis extends Component { return; } + const currentQueryParams = this.router.currentRoute.queryParams; + this.router.transitionTo(this.router.currentRoute.name, { + queryParams: { + ...currentQueryParams, + selectedChart: data.title, + }, + }); + this.selectedChart = data; this.showingSelectedChart = true; @@ -217,7 +246,10 @@ export default class AdminReportSentimentAnalysis extends Component { this.hasMorePosts = response.has_more; this.nextOffset = response.next_offset; - return response.posts.map((post) => Post.create(post)); + + const mappedPosts = response.posts.map((post) => Post.create(post)); + this.posts.pushObjects(mappedPosts); + return mappedPosts; } catch (e) { popupAjaxError(e); } @@ -228,9 +260,35 @@ export default class AdminReportSentimentAnalysis extends Component { this.showingSelectedChart = false; this.selectedChart = null; this.activeFilter = "all"; + this.posts = []; + + const currentQueryParams = this.router.currentRoute.queryParams; + this.router.transitionTo(this.router.currentRoute.name, { + queryParams: { + ...currentQueryParams, + selectedChart: null, + }, + }); + } + + @action + shareChart() { + const url = this.router.currentURL; + if (!url) { + return; + } + + clipboardCopy(getAbsoluteURL(url)); + this.shareIcon = "check"; + + discourseLater(() => { + this.shareIcon = "link"; + }, 2000); }