FEATURE: Emotion activity metrics table (#916)
This commit is contained in:
parent
b10be23533
commit
48d08dedd4
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="cell title">
|
||||||
|
{{#if this.model.icon}}
|
||||||
|
{{d-icon this.model.icon}}
|
||||||
|
{{/if}}
|
||||||
|
<a href="{{this.filterURL}}{{this.model.type}}">{{this.model.title}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cell value today-count">{{number this.model.todayCount}}</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cell value yesterday-count {{this.model.yesterdayTrend}}"
|
||||||
|
title={{this.model.yesterdayCountTitle}}
|
||||||
|
>
|
||||||
|
{{number this.model.yesterdayCount}}
|
||||||
|
{{d-icon this.model.yesterdayTrendIcon}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cell value sevendays-count {{this.model.sevenDaysTrend}}"
|
||||||
|
title={{this.model.sevenDaysCountTitle}}
|
||||||
|
>
|
||||||
|
{{number this.model.lastSevenDaysCount}}
|
||||||
|
{{d-icon this.model.sevenDaysTrendIcon}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cell value thirty-days-count {{this.model.thirtyDaysTrend}}"
|
||||||
|
title={{this.model.thirtyDaysCountTitle}}
|
||||||
|
>
|
||||||
|
{{number this.model.lastThirtyDaysCount}}
|
||||||
|
|
||||||
|
{{#if this.model.canDisplayTrendIcon}}
|
||||||
|
{{d-icon this.model.thirtyDaysTrendIcon}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
import Component from "@ember/component";
|
||||||
|
import { attributeBindings, classNames } from "@ember-decorators/component";
|
||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
|
@classNames("admin-report-counters")
|
||||||
|
@attributeBindings("model.description:title")
|
||||||
|
export default class AdminReportEmotion extends Component {
|
||||||
|
get filterURL() {
|
||||||
|
let aMonthAgo = moment().subtract(1, "month").format("YYYY-MM-DD");
|
||||||
|
return getURL(`/filter?q=activity-after%3A${aMonthAgo}%20order%3A`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,37 @@
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import AdminDashboardTabController from "admin/controllers/admin-dashboard-tab";
|
import AdminDashboardTabController from "admin/controllers/admin-dashboard-tab";
|
||||||
|
|
||||||
export default class AdminDashboardSentiment extends AdminDashboardTabController {
|
export default class AdminDashboardSentiment extends AdminDashboardTabController {
|
||||||
@computed("startDate", "endDate")
|
get emotions() {
|
||||||
get filters() {
|
const emotions = [
|
||||||
return { startDate: this.startDate, endDate: this.endDate };
|
"admiration",
|
||||||
|
"amusement",
|
||||||
|
"anger",
|
||||||
|
"annoyance",
|
||||||
|
"approval",
|
||||||
|
"caring",
|
||||||
|
"confusion",
|
||||||
|
"curiosity",
|
||||||
|
"desire",
|
||||||
|
"disappointment",
|
||||||
|
"disapproval",
|
||||||
|
"disgust",
|
||||||
|
"embarrassment",
|
||||||
|
"excitement",
|
||||||
|
"fear",
|
||||||
|
"gratitude",
|
||||||
|
"grief",
|
||||||
|
"joy",
|
||||||
|
"love",
|
||||||
|
"nervousness",
|
||||||
|
"neutral",
|
||||||
|
"optimism",
|
||||||
|
"pride",
|
||||||
|
"realization",
|
||||||
|
"relief",
|
||||||
|
"remorse",
|
||||||
|
"sadness",
|
||||||
|
"surprise",
|
||||||
|
];
|
||||||
|
return emotions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,43 @@
|
||||||
@filters={{this.filters}}
|
@filters={{this.filters}}
|
||||||
@showHeader={{true}}
|
@showHeader={{true}}
|
||||||
/>
|
/>
|
||||||
|
<div class="admin-report activity-metrics">
|
||||||
<AdminReport
|
<div class="header">
|
||||||
@dataSourceName="post_emotion"
|
<ul class="breadcrumb">
|
||||||
@filters={{this.filters}}
|
<li class="item report">
|
||||||
@showHeader={{true}}
|
<LinkTo @route="adminReports" class="report-url">
|
||||||
/>
|
{{i18n "admin.dashboard.emotion"}}
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="report-body">
|
||||||
|
<div class="counters-list">
|
||||||
|
<div class="counters-header">
|
||||||
|
<div class="counters-cell"></div>
|
||||||
|
<div class="counters-cell">{{i18n
|
||||||
|
"admin.dashboard.reports.today"
|
||||||
|
}}</div>
|
||||||
|
<div class="counters-cell">{{i18n
|
||||||
|
"admin.dashboard.reports.yesterday"
|
||||||
|
}}</div>
|
||||||
|
<div class="counters-cell">{{i18n
|
||||||
|
"admin.dashboard.reports.last_7_days"
|
||||||
|
}}</div>
|
||||||
|
<div class="counters-cell">{{i18n
|
||||||
|
"admin.dashboard.reports.last_30_days"
|
||||||
|
}}</div>
|
||||||
|
</div>
|
||||||
|
{{#each this.emotions as |metric|}}
|
||||||
|
<AdminReport
|
||||||
|
@showHeader={{false}}
|
||||||
|
@forcedModes="emotion"
|
||||||
|
@dataSourceName="emotion_{{metric}}"
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -4,5 +4,8 @@
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
grid-column-gap: 1em;
|
grid-column-gap: 1em;
|
||||||
grid-row-gap: 1em;
|
grid-row-gap: 1em;
|
||||||
|
.admin-report {
|
||||||
|
grid-column: span 12;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
.dashboard.dashboard-sentiment .charts {
|
|
||||||
.overall-sentiment {
|
|
||||||
grid-column: span 6;
|
|
||||||
}
|
|
||||||
.post-emotion {
|
|
||||||
grid-column: span 6;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
.dashboard.dashboard-sentiment {
|
|
||||||
.charts {
|
|
||||||
.overall-sentiment {
|
|
||||||
grid-column: span 12;
|
|
||||||
}
|
|
||||||
.post-emotion {
|
|
||||||
grid-column: span 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,6 +11,8 @@ en:
|
||||||
site_settings:
|
site_settings:
|
||||||
categories:
|
categories:
|
||||||
discourse_ai: "Discourse AI"
|
discourse_ai: "Discourse AI"
|
||||||
|
dashboard:
|
||||||
|
emotion: "Emotion"
|
||||||
js:
|
js:
|
||||||
discourse_automation:
|
discourse_automation:
|
||||||
scriptables:
|
scriptables:
|
||||||
|
|
|
@ -86,7 +86,7 @@ en:
|
||||||
ai_custom_summarization_allowed_groups: "Groups allowed to use create new summaries."
|
ai_custom_summarization_allowed_groups: "Groups allowed to use create new summaries."
|
||||||
ai_pm_summarization_allowed_groups: "Groups allowed to create and view summaries in PMs."
|
ai_pm_summarization_allowed_groups: "Groups allowed to create and view summaries in PMs."
|
||||||
ai_summarize_max_hot_topics_gists_per_batch: "After updating topics in the hot list, we'll generate brief summaries of the first N ones. (Disabled when 0)"
|
ai_summarize_max_hot_topics_gists_per_batch: "After updating topics in the hot list, we'll generate brief summaries of the first N ones. (Disabled when 0)"
|
||||||
ai_hot_topic_gists_allowed_groups: "Groups allowed to see gists in the hot topics list."
|
ai_hot_topic_gists_allowed_groups: "Groups allowed to see gists in the hot topics list."
|
||||||
ai_summary_backfill_maximum_topics_per_hour: "Number of topic summaries to backfill per hour."
|
ai_summary_backfill_maximum_topics_per_hour: "Number of topic summaries to backfill per hour."
|
||||||
|
|
||||||
ai_bot_enabled: "Enable the AI Bot module."
|
ai_bot_enabled: "Enable the AI Bot module."
|
||||||
|
@ -112,14 +112,65 @@ en:
|
||||||
reports:
|
reports:
|
||||||
overall_sentiment:
|
overall_sentiment:
|
||||||
title: "Overall sentiment"
|
title: "Overall sentiment"
|
||||||
description: "The chart compares the number of posts classified as either positive or negative. These are calculated when positive or negative scores > the set threshold score. This means neutral posts are not shown. Private messages (PMs) are also excluded. Classified with \"cardiffnlp/twitter-roberta-base-sentiment-latest\""
|
description: 'The chart compares the number of posts classified as either positive or negative. These are calculated when positive or negative scores > the set threshold score. This means neutral posts are not shown. Private messages (PMs) are also excluded. Classified with "cardiffnlp/twitter-roberta-base-sentiment-latest"'
|
||||||
xaxis: "Positive(%)"
|
xaxis: "Positive(%)"
|
||||||
yaxis: "Date"
|
yaxis: "Date"
|
||||||
post_emotion:
|
emotion_admiration:
|
||||||
title: "Post emotion"
|
title: Admiration
|
||||||
description: "Number of posts classified with one of the following emotions, grouped by poster's trust level. Posts that are not positive or negative and considered neutral, are not shown. Private messages (PMs) are also excluded. Classified with \"j-hartmann/emotion-english-roberta-large\""
|
emotion_amusement:
|
||||||
xaxis:
|
title: Amusement
|
||||||
yaxis:
|
emotion_anger:
|
||||||
|
title: Anger
|
||||||
|
emotion_annoyance:
|
||||||
|
title: Annoyance
|
||||||
|
emotion_approval:
|
||||||
|
title: Approval
|
||||||
|
emotion_caring:
|
||||||
|
title: Caring
|
||||||
|
emotion_confusion:
|
||||||
|
title: Confusion
|
||||||
|
emotion_curiosity:
|
||||||
|
title: Curiosity
|
||||||
|
emotion_desire:
|
||||||
|
title: Desire
|
||||||
|
emotion_disappointment:
|
||||||
|
title: Disappointment
|
||||||
|
emotion_disapproval:
|
||||||
|
title: Disapproval
|
||||||
|
emotion_disgust:
|
||||||
|
title: Disgust
|
||||||
|
emotion_embarrassment:
|
||||||
|
title: Embarrassment
|
||||||
|
emotion_excitement:
|
||||||
|
title: Excitement
|
||||||
|
emotion_fear:
|
||||||
|
title: Fear
|
||||||
|
emotion_gratitude:
|
||||||
|
title: Gratitude
|
||||||
|
emotion_grief:
|
||||||
|
title: Grief
|
||||||
|
emotion_joy:
|
||||||
|
title: Joy
|
||||||
|
emotion_love:
|
||||||
|
title: Love
|
||||||
|
emotion_nervousness:
|
||||||
|
title: Nervousness
|
||||||
|
emotion_neutral:
|
||||||
|
title: Neutral
|
||||||
|
emotion_optimism:
|
||||||
|
title: Optimism
|
||||||
|
emotion_pride:
|
||||||
|
title: Pride
|
||||||
|
emotion_realization:
|
||||||
|
title: Realization
|
||||||
|
emotion_relief:
|
||||||
|
title: Relief
|
||||||
|
emotion_remorse:
|
||||||
|
title: Remorse
|
||||||
|
emotion_sadness:
|
||||||
|
title: Sadness
|
||||||
|
emotion_surprise:
|
||||||
|
title: Surprise
|
||||||
|
|
||||||
discourse_ai:
|
discourse_ai:
|
||||||
unknown_model: "Unknown AI model"
|
unknown_model: "Unknown AI model"
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Sentiment
|
||||||
|
class EmotionDashboardReport
|
||||||
|
def self.register!(plugin)
|
||||||
|
Emotions::LIST.each do |emotion|
|
||||||
|
plugin.add_report("emotion_#{emotion}") do |report|
|
||||||
|
query_results = DiscourseAi::Sentiment::EmotionDashboardReport.fetch_data
|
||||||
|
report.data = query_results.pop(30).map { |row| { x: row.day, y: row.send(emotion) } }
|
||||||
|
report.prev30Days =
|
||||||
|
query_results.take(30).map { |row| { x: row.day, y: row.send(emotion) } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.fetch_data
|
||||||
|
DB.query(<<~SQL, end: Time.now.tomorrow.midnight, start: 60.days.ago.midnight)
|
||||||
|
SELECT
|
||||||
|
posts.created_at::DATE AS day,
|
||||||
|
#{
|
||||||
|
DiscourseAi::Sentiment::Emotions::LIST
|
||||||
|
.map do |emotion|
|
||||||
|
"COUNT(*) FILTER (WHERE (classification_results.classification::jsonb->'#{emotion}')::float > 0.1) AS #{emotion}"
|
||||||
|
end
|
||||||
|
.join(",\n ")
|
||||||
|
}
|
||||||
|
FROM
|
||||||
|
classification_results
|
||||||
|
INNER JOIN
|
||||||
|
posts ON posts.id = classification_results.target_id AND
|
||||||
|
posts.deleted_at IS NULL AND
|
||||||
|
posts.created_at BETWEEN :start AND :end
|
||||||
|
INNER JOIN
|
||||||
|
topics ON topics.id = posts.topic_id AND
|
||||||
|
topics.archetype = 'regular' AND
|
||||||
|
topics.deleted_at IS NULL
|
||||||
|
WHERE
|
||||||
|
classification_results.target_type = 'Post' AND
|
||||||
|
classification_results.model_used = 'SamLowe/roberta-base-go_emotions'
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 1 ASC
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,38 +4,7 @@ module DiscourseAi
|
||||||
module Sentiment
|
module Sentiment
|
||||||
class EmotionFilterOrder
|
class EmotionFilterOrder
|
||||||
def self.register!(plugin)
|
def self.register!(plugin)
|
||||||
emotions = %w[
|
Emotions::LIST.each do |emotion|
|
||||||
admiration
|
|
||||||
amusement
|
|
||||||
anger
|
|
||||||
annoyance
|
|
||||||
approval
|
|
||||||
caring
|
|
||||||
confusion
|
|
||||||
curiosity
|
|
||||||
desire
|
|
||||||
disappointment
|
|
||||||
disapproval
|
|
||||||
disgust
|
|
||||||
embarrassment
|
|
||||||
excitement
|
|
||||||
fear
|
|
||||||
gratitude
|
|
||||||
grief
|
|
||||||
joy
|
|
||||||
love
|
|
||||||
nervousness
|
|
||||||
neutral
|
|
||||||
optimism
|
|
||||||
pride
|
|
||||||
realization
|
|
||||||
relief
|
|
||||||
remorse
|
|
||||||
sadness
|
|
||||||
surprise
|
|
||||||
]
|
|
||||||
|
|
||||||
emotions.each do |emotion|
|
|
||||||
filter_order_emotion = ->(scope, order_direction) do
|
filter_order_emotion = ->(scope, order_direction) do
|
||||||
emotion_clause = <<~SQL
|
emotion_clause = <<~SQL
|
||||||
SUM(
|
SUM(
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Sentiment
|
||||||
|
class Emotions
|
||||||
|
LIST = %w[
|
||||||
|
admiration
|
||||||
|
amusement
|
||||||
|
anger
|
||||||
|
annoyance
|
||||||
|
approval
|
||||||
|
caring
|
||||||
|
confusion
|
||||||
|
curiosity
|
||||||
|
desire
|
||||||
|
disappointment
|
||||||
|
disapproval
|
||||||
|
disgust
|
||||||
|
embarrassment
|
||||||
|
excitement
|
||||||
|
fear
|
||||||
|
gratitude
|
||||||
|
grief
|
||||||
|
joy
|
||||||
|
love
|
||||||
|
nervousness
|
||||||
|
neutral
|
||||||
|
optimism
|
||||||
|
pride
|
||||||
|
realization
|
||||||
|
relief
|
||||||
|
remorse
|
||||||
|
sadness
|
||||||
|
surprise
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,122 +15,8 @@ module DiscourseAi
|
||||||
plugin.on(:post_edited, &sentiment_analysis_cb)
|
plugin.on(:post_edited, &sentiment_analysis_cb)
|
||||||
|
|
||||||
EmotionFilterOrder.register!(plugin)
|
EmotionFilterOrder.register!(plugin)
|
||||||
|
EmotionDashboardReport.register!(plugin)
|
||||||
plugin.add_report("overall_sentiment") do |report|
|
SentimentDashboardReport.register!(plugin)
|
||||||
report.modes = [:stacked_chart]
|
|
||||||
threshold = 0.6
|
|
||||||
|
|
||||||
sentiment_count_sql = Proc.new { |sentiment| <<~SQL }
|
|
||||||
COUNT(
|
|
||||||
CASE WHEN (cr.classification::jsonb->'#{sentiment}')::float > :threshold THEN 1 ELSE NULL END
|
|
||||||
) AS #{sentiment}_count
|
|
||||||
SQL
|
|
||||||
|
|
||||||
grouped_sentiments =
|
|
||||||
DB.query(
|
|
||||||
<<~SQL,
|
|
||||||
SELECT
|
|
||||||
DATE_TRUNC('day', p.created_at)::DATE AS posted_at,
|
|
||||||
#{sentiment_count_sql.call("positive")},
|
|
||||||
-#{sentiment_count_sql.call("negative")}
|
|
||||||
FROM
|
|
||||||
classification_results AS cr
|
|
||||||
INNER JOIN posts p ON p.id = cr.target_id AND cr.target_type = 'Post'
|
|
||||||
INNER JOIN topics t ON t.id = p.topic_id
|
|
||||||
INNER JOIN categories c ON c.id = t.category_id
|
|
||||||
WHERE
|
|
||||||
t.archetype = 'regular' AND
|
|
||||||
p.user_id > 0 AND
|
|
||||||
cr.model_used = 'cardiffnlp/twitter-roberta-base-sentiment-latest' AND
|
|
||||||
(p.created_at > :report_start AND p.created_at < :report_end)
|
|
||||||
GROUP BY DATE_TRUNC('day', p.created_at)
|
|
||||||
SQL
|
|
||||||
report_start: report.start_date,
|
|
||||||
report_end: report.end_date,
|
|
||||||
threshold: threshold,
|
|
||||||
)
|
|
||||||
|
|
||||||
data_points = %w[positive negative]
|
|
||||||
|
|
||||||
return report if grouped_sentiments.empty?
|
|
||||||
|
|
||||||
report.data =
|
|
||||||
data_points.map do |point|
|
|
||||||
{
|
|
||||||
req: "sentiment_#{point}",
|
|
||||||
color: point == "positive" ? report.colors[:lime] : report.colors[:purple],
|
|
||||||
label: I18n.t("discourse_ai.sentiment.reports.overall_sentiment.#{point}"),
|
|
||||||
data:
|
|
||||||
grouped_sentiments.map do |gs|
|
|
||||||
{ x: gs.posted_at, y: gs.public_send("#{point}_count") }
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
plugin.add_report("post_emotion") do |report|
|
|
||||||
report.modes = [:stacked_line_chart]
|
|
||||||
threshold = 0.3
|
|
||||||
|
|
||||||
emotion_count_clause = Proc.new { |emotion| <<~SQL }
|
|
||||||
COUNT(
|
|
||||||
CASE WHEN (cr.classification::jsonb->'#{emotion}')::float > :threshold THEN 1 ELSE NULL END
|
|
||||||
) AS #{emotion}_count
|
|
||||||
SQL
|
|
||||||
|
|
||||||
grouped_emotions =
|
|
||||||
DB.query(
|
|
||||||
<<~SQL,
|
|
||||||
SELECT
|
|
||||||
DATE_TRUNC('day', p.created_at)::DATE AS posted_at,
|
|
||||||
#{emotion_count_clause.call("sadness")},
|
|
||||||
#{emotion_count_clause.call("surprise")},
|
|
||||||
#{emotion_count_clause.call("fear")},
|
|
||||||
#{emotion_count_clause.call("anger")},
|
|
||||||
#{emotion_count_clause.call("joy")},
|
|
||||||
#{emotion_count_clause.call("disgust")}
|
|
||||||
FROM
|
|
||||||
classification_results AS cr
|
|
||||||
INNER JOIN posts p ON p.id = cr.target_id AND cr.target_type = 'Post'
|
|
||||||
INNER JOIN users u ON p.user_id = u.id
|
|
||||||
INNER JOIN topics t ON t.id = p.topic_id
|
|
||||||
INNER JOIN categories c ON c.id = t.category_id
|
|
||||||
WHERE
|
|
||||||
t.archetype = 'regular' AND
|
|
||||||
p.user_id > 0 AND
|
|
||||||
cr.model_used = 'j-hartmann/emotion-english-distilroberta-base' AND
|
|
||||||
(p.created_at > :report_start AND p.created_at < :report_end)
|
|
||||||
GROUP BY DATE_TRUNC('day', p.created_at)
|
|
||||||
SQL
|
|
||||||
report_start: report.start_date,
|
|
||||||
report_end: report.end_date,
|
|
||||||
threshold: threshold,
|
|
||||||
)
|
|
||||||
|
|
||||||
return report if grouped_emotions.empty?
|
|
||||||
|
|
||||||
emotions = [
|
|
||||||
{ name: "sadness", color: report.colors[:turquoise] },
|
|
||||||
{ name: "disgust", color: report.colors[:lime] },
|
|
||||||
{ name: "fear", color: report.colors[:purple] },
|
|
||||||
{ name: "anger", color: report.colors[:magenta] },
|
|
||||||
{ name: "joy", color: report.colors[:yellow] },
|
|
||||||
{ name: "surprise", color: report.colors[:brown] },
|
|
||||||
]
|
|
||||||
|
|
||||||
report.data =
|
|
||||||
emotions.map do |emotion|
|
|
||||||
{
|
|
||||||
req: "emotion_#{emotion[:name]}",
|
|
||||||
color: emotion[:color],
|
|
||||||
label: I18n.t("discourse_ai.sentiment.reports.post_emotion.#{emotion[:name]}"),
|
|
||||||
data:
|
|
||||||
grouped_emotions.map do |ge|
|
|
||||||
{ x: ge.posted_at, y: ge.public_send("#{emotion[:name]}_count") }
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Sentiment
|
||||||
|
class SentimentDashboardReport
|
||||||
|
def self.register!(plugin)
|
||||||
|
plugin.add_report("overall_sentiment") do |report|
|
||||||
|
report.modes = [:stacked_chart]
|
||||||
|
threshold = 0.6
|
||||||
|
|
||||||
|
sentiment_count_sql = Proc.new { |sentiment| <<~SQL }
|
||||||
|
COUNT(
|
||||||
|
CASE WHEN (cr.classification::jsonb->'#{sentiment}')::float > :threshold THEN 1 ELSE NULL END
|
||||||
|
) AS #{sentiment}_count
|
||||||
|
SQL
|
||||||
|
|
||||||
|
grouped_sentiments =
|
||||||
|
DB.query(
|
||||||
|
<<~SQL,
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('day', p.created_at)::DATE AS posted_at,
|
||||||
|
#{sentiment_count_sql.call("positive")},
|
||||||
|
-#{sentiment_count_sql.call("negative")}
|
||||||
|
FROM
|
||||||
|
classification_results AS cr
|
||||||
|
INNER JOIN posts p ON p.id = cr.target_id AND cr.target_type = 'Post'
|
||||||
|
INNER JOIN topics t ON t.id = p.topic_id
|
||||||
|
INNER JOIN categories c ON c.id = t.category_id
|
||||||
|
WHERE
|
||||||
|
t.archetype = 'regular' AND
|
||||||
|
p.user_id > 0 AND
|
||||||
|
cr.model_used = 'cardiffnlp/twitter-roberta-base-sentiment-latest' AND
|
||||||
|
(p.created_at > :report_start AND p.created_at < :report_end)
|
||||||
|
GROUP BY DATE_TRUNC('day', p.created_at)
|
||||||
|
SQL
|
||||||
|
report_start: report.start_date,
|
||||||
|
report_end: report.end_date,
|
||||||
|
threshold: threshold,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_points = %w[positive negative]
|
||||||
|
|
||||||
|
return report if grouped_sentiments.empty?
|
||||||
|
|
||||||
|
report.data =
|
||||||
|
data_points.map do |point|
|
||||||
|
{
|
||||||
|
req: "sentiment_#{point}",
|
||||||
|
color: point == "positive" ? report.colors[:lime] : report.colors[:purple],
|
||||||
|
label: I18n.t("discourse_ai.sentiment.reports.overall_sentiment.#{point}"),
|
||||||
|
data:
|
||||||
|
grouped_sentiments.map do |gs|
|
||||||
|
{ x: gs.posted_at, y: gs.public_send("#{point}_count") }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -32,8 +32,6 @@ register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.sc
|
||||||
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"
|
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"
|
||||||
|
|
||||||
register_asset "stylesheets/modules/sentiment/common/dashboard.scss"
|
register_asset "stylesheets/modules/sentiment/common/dashboard.scss"
|
||||||
register_asset "stylesheets/modules/sentiment/desktop/dashboard.scss", :desktop
|
|
||||||
register_asset "stylesheets/modules/sentiment/mobile/dashboard.scss", :mobile
|
|
||||||
|
|
||||||
register_asset "stylesheets/modules/llms/common/ai-llms-editor.scss"
|
register_asset "stylesheets/modules/llms/common/ai-llms-editor.scss"
|
||||||
|
|
||||||
|
|
|
@ -88,27 +88,69 @@ RSpec.describe DiscourseAi::Sentiment::EntryPoint do
|
||||||
describe "post_emotion report" do
|
describe "post_emotion report" do
|
||||||
let(:emotion_1) do
|
let(:emotion_1) do
|
||||||
{
|
{
|
||||||
sadness: 0.49,
|
love: 0.9444406,
|
||||||
surprise: 0.23,
|
admiration: 0.013724019,
|
||||||
neutral: 0.6,
|
surprise: 0.010188869,
|
||||||
fear: 0.34,
|
excitement: 0.007888741,
|
||||||
anger: 0.87,
|
curiosity: 0.006301749,
|
||||||
joy: 0.22,
|
joy: 0.004060776,
|
||||||
disgust: 0.70,
|
confusion: 0.0028238264,
|
||||||
|
approval: 0.0018160914,
|
||||||
|
realization: 0.001174849,
|
||||||
|
neutral: 0.0008561869,
|
||||||
|
amusement: 0.00075853954,
|
||||||
|
disapproval: 0.0006987994,
|
||||||
|
disappointment: 0.0006166883,
|
||||||
|
anger: 0.0006000542,
|
||||||
|
annoyance: 0.0005615011,
|
||||||
|
desire: 0.00046368592,
|
||||||
|
fear: 0.00045117878,
|
||||||
|
sadness: 0.00041727215,
|
||||||
|
gratitude: 0.00041727215,
|
||||||
|
optimism: 0.00037112957,
|
||||||
|
disgust: 0.00035552034,
|
||||||
|
nervousness: 0.00022954118,
|
||||||
|
embarrassment: 0.0002049572,
|
||||||
|
caring: 0.00017737568,
|
||||||
|
remorse: 0.00011407586,
|
||||||
|
grief: 0.0001006716,
|
||||||
|
pride: 0.00009681493,
|
||||||
|
relief: 0.00008919009,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:emotion_2) do
|
let(:emotion_2) do
|
||||||
{
|
{
|
||||||
sadness: 0.19,
|
love: 0.8444406,
|
||||||
surprise: 0.63,
|
admiration: 0.113724019,
|
||||||
neutral: 0.45,
|
surprise: 0.010188869,
|
||||||
fear: 0.44,
|
excitement: 0.007888741,
|
||||||
anger: 0.27,
|
curiosity: 0.006301749,
|
||||||
joy: 0.62,
|
joy: 0.004060776,
|
||||||
disgust: 0.30,
|
confusion: 0.0028238264,
|
||||||
|
approval: 0.0018160914,
|
||||||
|
realization: 0.001174849,
|
||||||
|
neutral: 0.0008561869,
|
||||||
|
amusement: 0.00075853954,
|
||||||
|
disapproval: 0.0006987994,
|
||||||
|
disappointment: 0.0006166883,
|
||||||
|
anger: 0.0006000542,
|
||||||
|
annoyance: 0.0005615011,
|
||||||
|
desire: 0.00046368592,
|
||||||
|
fear: 0.00045117878,
|
||||||
|
sadness: 0.00041727215,
|
||||||
|
gratitude: 0.00041727215,
|
||||||
|
optimism: 0.00037112957,
|
||||||
|
disgust: 0.00035552034,
|
||||||
|
nervousness: 0.00022954118,
|
||||||
|
embarrassment: 0.0002049572,
|
||||||
|
caring: 0.00017737568,
|
||||||
|
remorse: 0.00011407586,
|
||||||
|
grief: 0.0001006716,
|
||||||
|
pride: 0.00009681493,
|
||||||
|
relief: 0.00008919009,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:model_used) { "j-hartmann/emotion-english-distilroberta-base" }
|
let(:model_used) { "SamLowe/roberta-base-go_emotions" }
|
||||||
|
|
||||||
def emotion_classification(post, classification)
|
def emotion_classification(post, classification)
|
||||||
Fabricate(
|
Fabricate(
|
||||||
|
@ -125,22 +167,19 @@ RSpec.describe DiscourseAi::Sentiment::EntryPoint do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calculate averages using only public posts" do
|
it "calculate averages using only public posts" do
|
||||||
threshold = 0.30
|
threshold = 0.10
|
||||||
|
|
||||||
emotion_classification(post_1, emotion_1)
|
emotion_classification(post_1, emotion_1)
|
||||||
emotion_classification(post_2, emotion_2)
|
emotion_classification(post_2, emotion_2)
|
||||||
emotion_classification(pm, emotion_2)
|
emotion_classification(pm, emotion_2)
|
||||||
|
|
||||||
report = Report.find("post_emotion")
|
report = Report.find("emotion_love")
|
||||||
|
|
||||||
data_point = report.data
|
data_point = report.data
|
||||||
|
|
||||||
data_point.each do |point|
|
data_point.each do |point|
|
||||||
emotion = strip_emoji_and_downcase(point[:label])
|
expected = (emotion_1[:love] > threshold ? 1 : 0) + (emotion_2[:love] > threshold ? 1 : 0)
|
||||||
expected =
|
expect(point[:y]).to eq(expected)
|
||||||
(emotion_1[emotion.to_sym] > threshold ? 1 : 0) +
|
|
||||||
(emotion_2[emotion.to_sym] > threshold ? 1 : 0)
|
|
||||||
expect(point[:data][0][:y]).to eq(expected)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue