Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV: Updates to sentiment analysis reports #1161

Merged
merged 15 commits into from
Mar 5, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def posts
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,
Expand Down
4 changes: 4 additions & 0 deletions app/models/classification_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"];
}
Expand Down Expand Up @@ -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;
});
}
Expand Down Expand Up @@ -186,13 +171,57 @@ 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) {
// Don't do anything if the same chart is clicked again
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;

Expand All @@ -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);
}
Expand All @@ -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);
}

<template>
<span {{didInsert this.openToChart}}></span>

{{#unless this.showingSelectedChart}}
<div class="admin-report-sentiment-analysis">
{{#each this.transformedData as |data|}}
Expand All @@ -252,6 +310,7 @@ export default class AdminReportSentimentAnalysis extends Component {
@data={{data.scores}}
@totalScore={{data.total_score}}
@doughnutTitle={{data.title}}
@displayLegend={{true}}
/>
</div>
{{/each}}
Expand All @@ -260,20 +319,33 @@ export default class AdminReportSentimentAnalysis extends Component {

{{#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}}
/>
<div class="admin-report-sentiment-analysis__selected-chart-actions">
<DButton
@label="back_button"
@icon="chevron-left"
class="btn-flat"
@action={{this.backToAllCharts}}
/>

<DTooltip
class="share btn-flat"
@icon={{this.shareIcon}}
{{on "click" this.shareChart}}
@content={{i18n
"discourse_ai.sentiments.sentiment_analysis.share_chart"
}}
/>
</div>

<DoughnutChart
@labels={{@model.labels}}
@colors={{this.colors}}
@data={{this.selectedChart.scores}}
@totalScore={{this.selectedChart.total_score}}
@doughnutTitle={{this.selectedChart.title}}
@displayLegend={{true}}
/>

</div>
<div class="admin-report-sentiment-analysis-details">
<HorizontalOverflowNav
Expand Down
9 changes: 8 additions & 1 deletion assets/javascripts/discourse/components/doughnut-chart.gjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import Chart from "admin/components/chart";

export default class DoughnutChart extends Component {
@tracked canvasSize = null;

get config() {
const totalScore = this.args.totalScore || "";

Expand All @@ -13,14 +16,18 @@ export default class DoughnutChart extends Component {
{
data: this.args.data,
backgroundColor: this.args.colors,
cutout: "50%",
radius: 100,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: this.args.legendPosition || "bottom",
display: this.args.displayLegend || false,
position: "bottom",
},
},
},
Expand Down
7 changes: 7 additions & 0 deletions assets/javascripts/initializers/admin-reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export default {
"sentiment_analysis",
AdminReportSentimentAnalysis
);

api.registerValueTransformer(
"admin-reports-show-query-params",
({ value }) => {
return [...value, "selectedChart"];
}
);
});
},
};
36 changes: 21 additions & 15 deletions assets/javascripts/initializers/ai-sentiment-admin-nav.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { apiInitializer } from "discourse/lib/api";

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

if (settings.ai_sentiment_enabled) {
api.addAdminSidebarSectionLink("reports", {
name: "sentiment_overview",
route: "admin.dashboardSentiment",
label: "discourse_ai.sentiments.sidebar.overview",
icon: "chart-column",
});
api.addAdminSidebarSectionLink("reports", {
name: "sentiment_analysis",
route: "adminReports.show",
routeModels: ["sentiment_analysis"],
label: "discourse_ai.sentiments.sidebar.analysis",
icon: "chart-pie",
});
if (
!currentUser ||
!currentUser.admin ||
!currentUser.can_see_sentiment_reports
) {
return;
}

api.addAdminSidebarSectionLink("reports", {
name: "sentiment_overview",
route: "admin.dashboardSentiment",
label: "discourse_ai.sentiments.sidebar.overview",
icon: "chart-column",
});
api.addAdminSidebarSectionLink("reports", {
name: "sentiment_analysis",
route: "adminReports.show",
routeModels: ["sentiment_analysis"],
label: "discourse_ai.sentiments.sidebar.analysis",
icon: "chart-pie",
});
});
Loading