mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-03-01 14:59:22 +00:00
DEV: Sentiment analysis report follow-up updates (#1145)
* DEV: make include subcategories checkbox operational * DEV: add pagination for post requests * WIP: selected chart UX improvements * DEV: Functional sentiment filters * DEV: Reset filters after going back * DEV: Add category colors, improve UX * DEV: Update spec
This commit is contained in:
parent
43cbb7f45f
commit
08377bab35
@ -6,6 +6,9 @@ module DiscourseAi
|
|||||||
include Constants
|
include Constants
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
|
DEFAULT_POSTS_LIMIT = 50
|
||||||
|
MAX_POSTS_LIMIT = 100
|
||||||
|
|
||||||
def posts
|
def posts
|
||||||
group_by = params.required(:group_by)&.to_sym
|
group_by = params.required(:group_by)&.to_sym
|
||||||
group_value = params.required(:group_value).presence
|
group_value = params.required(:group_value).presence
|
||||||
@ -15,10 +18,13 @@ module DiscourseAi
|
|||||||
|
|
||||||
raise Discourse::InvalidParameters if %i[category tag].exclude?(group_by)
|
raise Discourse::InvalidParameters if %i[category tag].exclude?(group_by)
|
||||||
|
|
||||||
|
limit = fetch_limit_from_params(default: DEFAULT_POSTS_LIMIT, max: MAX_POSTS_LIMIT)
|
||||||
|
offset = params[:offset].to_i || 0
|
||||||
|
|
||||||
case group_by
|
case group_by
|
||||||
when :category
|
when :category
|
||||||
grouping_clause = "c.name"
|
grouping_clause = "c.name"
|
||||||
grouping_join = "INNER JOIN categories c ON c.id = t.category_id"
|
grouping_join = "" # categories already joined
|
||||||
when :tag
|
when :tag
|
||||||
grouping_clause = "tags.name"
|
grouping_clause = "tags.name"
|
||||||
grouping_join =
|
grouping_join =
|
||||||
@ -38,6 +44,11 @@ module DiscourseAi
|
|||||||
u.username,
|
u.username,
|
||||||
u.name,
|
u.name,
|
||||||
u.uploaded_avatar_id,
|
u.uploaded_avatar_id,
|
||||||
|
c.id AS category_id,
|
||||||
|
c.name AS category_name,
|
||||||
|
c.color AS category_color,
|
||||||
|
c.slug AS category_slug,
|
||||||
|
c.description AS category_description,
|
||||||
(CASE
|
(CASE
|
||||||
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
|
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
|
||||||
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
|
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
|
||||||
@ -47,6 +58,7 @@ module DiscourseAi
|
|||||||
INNER JOIN topics t ON t.id = p.topic_id
|
INNER JOIN topics t ON t.id = p.topic_id
|
||||||
INNER JOIN classification_results cr ON cr.target_id = p.id AND cr.target_type = 'Post'
|
INNER JOIN classification_results cr ON cr.target_id = p.id AND cr.target_type = 'Post'
|
||||||
LEFT JOIN users u ON u.id = p.user_id
|
LEFT JOIN users u ON u.id = p.user_id
|
||||||
|
LEFT JOIN categories c ON c.id = t.category_id
|
||||||
#{grouping_join}
|
#{grouping_join}
|
||||||
WHERE
|
WHERE
|
||||||
#{grouping_clause} = :group_value AND
|
#{grouping_clause} = :group_value AND
|
||||||
@ -56,22 +68,31 @@ module DiscourseAi
|
|||||||
((:start_date IS NULL OR p.created_at > :start_date) AND (:end_date IS NULL OR p.created_at < :end_date))
|
((:start_date IS NULL OR p.created_at > :start_date) AND (:end_date IS NULL OR p.created_at < :end_date))
|
||||||
AND p.deleted_at IS NULL
|
AND p.deleted_at IS NULL
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
SQL
|
SQL
|
||||||
group_value: group_value,
|
group_value: group_value,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
end_date: end_date,
|
end_date: end_date,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
|
limit: limit + 1,
|
||||||
|
offset: offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
has_more = posts.length > limit
|
||||||
|
posts.pop if has_more
|
||||||
|
|
||||||
render_json_dump(
|
render_json_dump(
|
||||||
serialize_data(
|
posts:
|
||||||
posts,
|
serialize_data(
|
||||||
AiSentimentPostSerializer,
|
posts,
|
||||||
scope: guardian,
|
AiSentimentPostSerializer,
|
||||||
add_raw: true,
|
scope: guardian,
|
||||||
add_excerpt: true,
|
add_raw: true,
|
||||||
add_title: true,
|
add_excerpt: true,
|
||||||
),
|
add_title: true,
|
||||||
|
),
|
||||||
|
has_more: has_more,
|
||||||
|
next_offset: has_more ? offset + limit : nil,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,7 +10,8 @@ class AiSentimentPostSerializer < ApplicationSerializer
|
|||||||
:avatar_template,
|
:avatar_template,
|
||||||
:excerpt,
|
:excerpt,
|
||||||
:sentiment,
|
:sentiment,
|
||||||
:truncated
|
:truncated,
|
||||||
|
:category
|
||||||
|
|
||||||
def avatar_template
|
def avatar_template
|
||||||
User.avatar_template(object.username, object.uploaded_avatar_id)
|
User.avatar_template(object.username, object.uploaded_avatar_id)
|
||||||
@ -23,4 +24,14 @@ class AiSentimentPostSerializer < ApplicationSerializer
|
|||||||
def truncated
|
def truncated
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def category
|
||||||
|
{
|
||||||
|
id: object.category_id,
|
||||||
|
name: object.category_name,
|
||||||
|
color: object.category_color,
|
||||||
|
slug: object.category_slug,
|
||||||
|
description: object.category_description,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,11 @@ import Component from "@glimmer/component";
|
|||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { fn, hash } from "@ember/helper";
|
import { fn, hash } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
import { action, get } from "@ember/object";
|
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 PostList from "discourse/components/post-list";
|
||||||
import dIcon from "discourse/helpers/d-icon";
|
import dIcon from "discourse/helpers/d-icon";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
@ -15,15 +19,88 @@ import DoughnutChart from "discourse/plugins/discourse-ai/discourse/components/d
|
|||||||
export default class AdminReportSentimentAnalysis extends Component {
|
export default class AdminReportSentimentAnalysis extends Component {
|
||||||
@tracked selectedChart = null;
|
@tracked selectedChart = null;
|
||||||
@tracked posts = null;
|
@tracked posts = null;
|
||||||
|
@tracked hasMorePosts = false;
|
||||||
|
@tracked nextOffset = 0;
|
||||||
|
@tracked showingSelectedChart = false;
|
||||||
|
@tracked activeFilter = "all";
|
||||||
|
|
||||||
get colors() {
|
setActiveFilter = modifier((element) => {
|
||||||
return ["#2ecc71", "#95a5a6", "#e74c3c"];
|
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) {
|
calculateNeutralScore(data) {
|
||||||
return data.total_count - (data.positive_count + data.negative_count);
|
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() {
|
get currentGroupFilter() {
|
||||||
return this.args.model.available_filters.find(
|
return this.args.model.available_filters.find(
|
||||||
(filter) => filter.id === "group_by"
|
(filter) => filter.id === "group_by"
|
||||||
@ -50,120 +127,179 @@ export default class AdminReportSentimentAnalysis extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@action
|
||||||
async showDetails(data) {
|
async showDetails(data) {
|
||||||
this.selectedChart = data;
|
if (this.selectedChart === data) {
|
||||||
try {
|
// Don't do anything if the same chart is clicked again
|
||||||
const posts = await ajax(`/discourse-ai/sentiment/posts`, {
|
return;
|
||||||
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));
|
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) {
|
} catch (e) {
|
||||||
popupAjaxError(e);
|
popupAjaxError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sentimentMapping(sentiment) {
|
@action
|
||||||
switch (sentiment) {
|
async fetchMorePosts() {
|
||||||
case "positive":
|
if (!this.hasMorePosts || this.selectedChart === null) {
|
||||||
return {
|
return [];
|
||||||
id: "positive",
|
}
|
||||||
text: i18n(
|
|
||||||
"discourse_ai.sentiments.sentiment_analysis.score_types.positive"
|
try {
|
||||||
),
|
const response = await this.postRequest();
|
||||||
icon: "face-smile",
|
|
||||||
};
|
this.hasMorePosts = response.has_more;
|
||||||
case "neutral":
|
this.nextOffset = response.next_offset;
|
||||||
return {
|
return response.posts.map((post) => Post.create(post));
|
||||||
id: "neutral",
|
} catch (e) {
|
||||||
text: i18n(
|
popupAjaxError(e);
|
||||||
"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) {
|
@action
|
||||||
if (data?.total_score) {
|
backToAllCharts() {
|
||||||
return `${data.title} (${data.total_score})`;
|
this.showingSelectedChart = false;
|
||||||
} else {
|
this.selectedChart = null;
|
||||||
return data.title;
|
this.activeFilter = "all";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-report-sentiment-analysis">
|
{{#unless this.showingSelectedChart}}
|
||||||
{{#each this.transformedData as |data|}}
|
<div class="admin-report-sentiment-analysis">
|
||||||
<div
|
{{#each this.transformedData as |data|}}
|
||||||
class="admin-report-sentiment-analysis__chart-wrapper"
|
<div
|
||||||
role="button"
|
class="admin-report-sentiment-analysis__chart-wrapper"
|
||||||
{{on "click" (fn this.showDetails data)}}
|
role="button"
|
||||||
{{closeOnClickOutside
|
{{on "click" (fn this.showDetails data)}}
|
||||||
(fn (mut this.selectedChart) null)
|
{{closeOnClickOutside
|
||||||
(hash
|
(fn (mut this.selectedChart) null)
|
||||||
targetSelector=".admin-report-sentiment-analysis-details"
|
(hash
|
||||||
secondaryTargetSelector=".admin-report-sentiment-analysis"
|
targetSelector=".admin-report-sentiment-analysis-details"
|
||||||
)
|
secondaryTargetSelector=".admin-report-sentiment-analysis"
|
||||||
}}
|
)
|
||||||
>
|
}}
|
||||||
<DoughnutChart
|
>
|
||||||
@labels={{@model.labels}}
|
<DoughnutChart
|
||||||
@colors={{this.colors}}
|
@labels={{@model.labels}}
|
||||||
@data={{data.scores}}
|
@colors={{this.colors}}
|
||||||
@doughnutTitle={{this.doughnutTitle data}}
|
@data={{data.scores}}
|
||||||
/>
|
@doughnutTitle={{this.doughnutTitle data}}
|
||||||
</div>
|
/>
|
||||||
{{/each}}
|
</div>
|
||||||
</div>
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
{{#if this.selectedChart}}
|
{{#if (and this.selectedChart this.showingSelectedChart)}}
|
||||||
<div class="admin-report-sentiment-analysis-details">
|
<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">
|
<h3 class="admin-report-sentiment-analysis-details__title">
|
||||||
{{this.selectedChart.title}}
|
{{this.selectedChart.title}}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ul class="admin-report-sentiment-analysis-details__scores">
|
<DoughnutChart
|
||||||
<li>
|
@labels={{@model.labels}}
|
||||||
{{dIcon "face-smile" style="color: #2ecc71"}}
|
@colors={{this.colors}}
|
||||||
{{i18n
|
@data={{this.selectedChart.scores}}
|
||||||
"discourse_ai.sentiments.sentiment_analysis.score_types.positive"
|
@doughnutTitle={{this.doughnutTitle this.selectedChart}}
|
||||||
}}:
|
/>
|
||||||
{{get this.selectedChart.scores 0}}</li>
|
</div>
|
||||||
<li>
|
<div class="admin-report-sentiment-analysis-details">
|
||||||
{{dIcon "face-meh"}}
|
<HorizontalOverflowNav
|
||||||
{{i18n
|
{{this.setActiveFilter}}
|
||||||
"discourse_ai.sentiments.sentiment_analysis.score_types.neutral"
|
class="admin-report-sentiment-analysis-details__filters"
|
||||||
}}:
|
>
|
||||||
{{get this.selectedChart.scores 1}}</li>
|
{{#each this.postFilters as |filter|}}
|
||||||
<li>
|
<li data-filter-type={{filter.id}}>
|
||||||
{{dIcon "face-angry"}}
|
<DButton
|
||||||
{{i18n
|
@icon={{filter.icon}}
|
||||||
"discourse_ai.sentiments.sentiment_analysis.score_types.negative"
|
@translatedLabel={{filter.text}}
|
||||||
}}:
|
@action={{filter.action}}
|
||||||
{{get this.selectedChart.scores 2}}</li>
|
class="btn-transparent"
|
||||||
</ul>
|
/>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</HorizontalOverflowNav>
|
||||||
|
|
||||||
<PostList
|
<PostList
|
||||||
@posts={{this.posts}}
|
@posts={{this.filteredPosts}}
|
||||||
@urlPath="url"
|
@urlPath="url"
|
||||||
@idPath="post_id"
|
@idPath="post_id"
|
||||||
@titlePath="topic_title"
|
@titlePath="topic_title"
|
||||||
@usernamePath="username"
|
@usernamePath="username"
|
||||||
|
@fetchMorePosts={{this.fetchMorePosts}}
|
||||||
class="admin-report-sentiment-analysis-details__post-list"
|
class="admin-report-sentiment-analysis-details__post-list"
|
||||||
>
|
>
|
||||||
<:abovePostItemExcerpt as |post|>
|
<:abovePostItemExcerpt as |post|>
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
.main {
|
.main {
|
||||||
flex: 100%;
|
flex: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
order: 2;
|
order: 2;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -83,6 +84,12 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__selected-chart {
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
border-radius: var(--d-border-radius);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -93,14 +100,34 @@
|
|||||||
|
|
||||||
.admin-report-sentiment-analysis-details {
|
.admin-report-sentiment-analysis-details {
|
||||||
@include report-container-box();
|
@include report-container-box();
|
||||||
flex: 1;
|
flex: 1 1 300px;
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
&__filters {
|
||||||
|
border-bottom: 1px solid var(--primary-low);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@include breakpoint("mobile-extra-large") {
|
||||||
|
.d-button-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: var(--font-up-2);
|
font-size: var(--font-up-2);
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-top: 1px solid var(--primary-low);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__scores {
|
&__scores {
|
||||||
@ -152,6 +179,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__post-list {
|
&__post-list {
|
||||||
|
margin-top: 1rem;
|
||||||
.avatar-wrapper,
|
.avatar-wrapper,
|
||||||
.avatar-link {
|
.avatar-link {
|
||||||
width: calc(48px * 0.75);
|
width: calc(48px * 0.75);
|
||||||
|
@ -651,7 +651,8 @@ en:
|
|||||||
dashboard:
|
dashboard:
|
||||||
title: "Sentiment"
|
title: "Sentiment"
|
||||||
sentiment_analysis:
|
sentiment_analysis:
|
||||||
score_types:
|
filter_types:
|
||||||
|
all: "All"
|
||||||
positive: "Positive"
|
positive: "Positive"
|
||||||
neutral: "Neutral"
|
neutral: "Neutral"
|
||||||
negative: "Negative"
|
negative: "Negative"
|
||||||
|
@ -31,7 +31,8 @@ module DiscourseAi
|
|||||||
auto_insert_none_item: false,
|
auto_insert_none_item: false,
|
||||||
)
|
)
|
||||||
|
|
||||||
report.add_category_filter(disabled: group_by_filter.to_sym == :tag)
|
category_id, include_subcategories =
|
||||||
|
report.add_category_filter(disabled: group_by_filter.to_sym == :tag)
|
||||||
|
|
||||||
tag_filter = report.filters.dig(:tag) || "any"
|
tag_filter = report.filters.dig(:tag) || "any"
|
||||||
tag_choices =
|
tag_choices =
|
||||||
@ -49,7 +50,8 @@ module DiscourseAi
|
|||||||
disabled: group_by_filter.to_sym == :category,
|
disabled: group_by_filter.to_sym == :category,
|
||||||
)
|
)
|
||||||
|
|
||||||
sentiment_data = DiscourseAi::Sentiment::SentimentAnalysisReport.fetch_data(report)
|
opts = { category_id: category_id, include_subcategories: include_subcategories }
|
||||||
|
sentiment_data = DiscourseAi::Sentiment::SentimentAnalysisReport.fetch_data(report, opts)
|
||||||
|
|
||||||
report.data = sentiment_data
|
report.data = sentiment_data
|
||||||
report.labels = [
|
report.labels = [
|
||||||
@ -60,7 +62,7 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.fetch_data(report)
|
def self.fetch_data(report, opts)
|
||||||
threshold = SENTIMENT_THRESHOLD
|
threshold = SENTIMENT_THRESHOLD
|
||||||
|
|
||||||
grouping = (report.filters.dig(:group_by) || GROUP_BY_FILTER_DEFAULT).to_sym
|
grouping = (report.filters.dig(:group_by) || GROUP_BY_FILTER_DEFAULT).to_sym
|
||||||
@ -118,6 +120,10 @@ module DiscourseAi
|
|||||||
when :category
|
when :category
|
||||||
if category_filter.nil?
|
if category_filter.nil?
|
||||||
""
|
""
|
||||||
|
elsif opts[:include_subcategories]
|
||||||
|
<<~SQL
|
||||||
|
AND (c.id = :category_filter OR c.parent_category_id = :category_filter)
|
||||||
|
SQL
|
||||||
else
|
else
|
||||||
"AND c.id = :category_filter"
|
"AND c.id = :category_filter"
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,8 @@ RSpec.describe DiscourseAi::Sentiment::SentimentController do
|
|||||||
|
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
|
|
||||||
posts = JSON.parse(response.body)
|
post_response = JSON.parse(response.body)
|
||||||
|
posts = post_response["posts"]
|
||||||
posts.each do |post|
|
posts.each do |post|
|
||||||
expect(post).to have_key("sentiment")
|
expect(post).to have_key("sentiment")
|
||||||
expect(post["sentiment"]).to match(/positive|negative|neutral/)
|
expect(post["sentiment"]).to match(/positive|negative|neutral/)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user