From d9f9e0c461a28781f38ea877177a187e35e2b059 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Tue, 2 Dec 2025 11:41:30 -0300 Subject: [PATCH] UX: New visualizations for new reports (#34) This adds some new visualizations and styles and reports and cleans up some existing styles and class names. Daily activity rhythm image Helping new members image Chat activity image Assigns image AI usage image Invitations image Writing analysis image --------- Co-authored-by: awesomerobot Co-authored-by: Martin Brennan --- .../action/new_user_interactions.rb | 57 +-- .../action/writing_analysis.rb | 115 +++++ .../discourse_rewind/fetch_reports.rb | 1 + .../components/reports/activity-calendar.gjs | 10 +- .../discourse/components/reports/ai-usage.gjs | 176 +++++++ .../components/reports/assignments.gjs | 63 +++ .../components/reports/best-posts.gjs | 24 +- .../components/reports/best-topics.gjs | 12 +- .../components/reports/chat-usage.gjs | 161 ++++++ .../components/reports/favorite-gifs.gjs | 53 ++ .../discourse/components/reports/fbff.gjs | 8 +- .../discourse/components/reports/invites.gjs | 118 +++++ .../reports/most-viewed-categories.gjs | 8 +- .../components/reports/most-viewed-tags.gjs | 2 +- .../reports/new-user-interactions.gjs | 30 ++ .../components/reports/reactions.gjs | 28 +- .../components/reports/reading-time.gjs | 8 +- .../reports/time-of-day-activity.gjs | 457 ++++++++++++++++++ .../components/reports/top-words.gjs | 41 +- .../reports/top-words/word-card.gjs | 24 +- .../components/reports/writing-analysis.gjs | 172 +++++++ .../discourse/components/rewind.gjs | 81 +++- assets/stylesheets/common/_index.scss | 7 + .../stylesheets/common/activity-calendar.scss | 44 +- assets/stylesheets/common/ai-usage.scss | 112 +++++ assets/stylesheets/common/assignments.scss | 107 ++++ assets/stylesheets/common/best-posts.scss | 20 +- assets/stylesheets/common/best-topics.scss | 3 +- assets/stylesheets/common/chat-usage.scss | 167 +++++++ assets/stylesheets/common/fbff.scss | 4 +- assets/stylesheets/common/folder-styles.scss | 4 +- assets/stylesheets/common/invites.scss | 221 +++++++++ .../common/most-viewed-categories.scss | 2 +- .../stylesheets/common/most-viewed-tags.scss | 2 +- .../common/new-user-interactions.scss | 77 +++ .../common/post-received-reactions.scss | 4 +- .../common/post-used-reactions.scss | 4 +- assets/stylesheets/common/reading-time.scss | 2 + assets/stylesheets/common/report.scss | 2 +- assets/stylesheets/common/rewind-callout.scss | 8 + assets/stylesheets/common/rewind-header.scss | 12 +- assets/stylesheets/common/rewind.scss | 6 +- .../common/time-of-day-activity.scss | 247 ++++++++++ assets/stylesheets/common/top-words.scss | 14 +- .../stylesheets/common/writing-analysis.scss | 94 ++++ config/locales/client.en.yml | 126 +++++ plugin.rb | 2 + public/images/dancing_baby.gif | Bin 0 -> 332925 bytes spec/actions/best_topics_spec.rb | 11 +- 49 files changed, 2761 insertions(+), 190 deletions(-) create mode 100644 app/services/discourse_rewind/action/writing_analysis.rb create mode 100644 assets/javascripts/discourse/components/reports/ai-usage.gjs create mode 100644 assets/javascripts/discourse/components/reports/assignments.gjs create mode 100644 assets/javascripts/discourse/components/reports/chat-usage.gjs create mode 100644 assets/javascripts/discourse/components/reports/favorite-gifs.gjs create mode 100644 assets/javascripts/discourse/components/reports/invites.gjs create mode 100644 assets/javascripts/discourse/components/reports/new-user-interactions.gjs create mode 100644 assets/javascripts/discourse/components/reports/time-of-day-activity.gjs create mode 100644 assets/javascripts/discourse/components/reports/writing-analysis.gjs create mode 100644 assets/stylesheets/common/ai-usage.scss create mode 100644 assets/stylesheets/common/assignments.scss create mode 100644 assets/stylesheets/common/chat-usage.scss create mode 100644 assets/stylesheets/common/invites.scss create mode 100644 assets/stylesheets/common/new-user-interactions.scss create mode 100644 assets/stylesheets/common/time-of-day-activity.scss create mode 100644 assets/stylesheets/common/writing-analysis.scss create mode 100644 public/images/dancing_baby.gif diff --git a/app/services/discourse_rewind/action/new_user_interactions.rb b/app/services/discourse_rewind/action/new_user_interactions.rb index 96ce495..30cd7c1 100644 --- a/app/services/discourse_rewind/action/new_user_interactions.rb +++ b/app/services/discourse_rewind/action/new_user_interactions.rb @@ -33,66 +33,45 @@ module DiscourseRewind return if new_user_ids.empty? # Count likes given to new users - likes_scope = - UserAction.where( - acting_user_id: user.id, - user_id: new_user_ids, - action_type: UserAction::WAS_LIKED, - ).where(created_at: date) - likes_given = likes_scope.count - liked_user_ids = likes_scope.distinct.pluck(:user_id) + liked_user_ids = + UserAction + .where( + acting_user_id: user.id, + user_id: new_user_ids, + action_type: UserAction::WAS_LIKED, + ) + .where(created_at: date) + .distinct + .pluck(:user_id) # Count replies to new users' posts - replies_scope = + replied_user_ids = Post .joins( "INNER JOIN posts AS parent_posts ON posts.reply_to_post_number = parent_posts.post_number AND posts.topic_id = parent_posts.topic_id", ) .where(posts: { user_id: user.id, deleted_at: nil, created_at: date }) .where("parent_posts.user_id": new_user_ids) - replies_to_new_users = replies_scope.count - replied_user_ids = replies_scope.distinct.pluck("parent_posts.user_id") - - # Count topics created by user that new users participated in - topics_with_new_users = - Topic - .joins(:posts) - .where(topics: { user_id: user.id, deleted_at: nil }) - .where(posts: { user_id: new_user_ids, deleted_at: nil }) - .where(topics: { created_at: date }) .distinct - .count + .pluck("parent_posts.user_id") - # Count direct messages/mentions to new users - mentions_scope = + # Count direct mentions to new users + mentioned_user_ids = Post .joins( "INNER JOIN user_actions ON user_actions.target_post_id = posts.id AND user_actions.action_type = #{UserAction::MENTION}", ) .where(posts: { user_id: user.id, deleted_at: nil, created_at: date }) .where(user_actions: { user_id: new_user_ids }) - mentions_to_new_users = mentions_scope.distinct.count - mentioned_user_ids = mentions_scope.distinct.pluck("user_actions.user_id") + .distinct + .pluck("user_actions.user_id") # Unique new users interacted with unique_new_users = (liked_user_ids + replied_user_ids + mentioned_user_ids).uniq.count - total_interactions = likes_given + replies_to_new_users + mentions_to_new_users + return if unique_new_users == 0 - return if total_interactions == 0 - - { - data: { - total_interactions: total_interactions, - likes_given: likes_given, - replies_to_new_users: replies_to_new_users, - mentions_to_new_users: mentions_to_new_users, - topics_with_new_users: topics_with_new_users, - unique_new_users: unique_new_users, - new_users_count: new_user_ids.count, - }, - identifier: "new-user-interactions", - } + { data: { unique_new_users: unique_new_users }, identifier: "new-user-interactions" } end end end diff --git a/app/services/discourse_rewind/action/writing_analysis.rb b/app/services/discourse_rewind/action/writing_analysis.rb new file mode 100644 index 0000000..86aaf6f --- /dev/null +++ b/app/services/discourse_rewind/action/writing_analysis.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +module DiscourseRewind + module Action + class WritingAnalysis < BaseReport + FakeData = { + data: { + total_words: 45_230, + total_posts: 197, + average_post_length: 230, + readability_score: 65.4, + }, + identifier: "writing-analysis", + } + + def call + return FakeData if Rails.env.development? + + total_words = + DB.query_single(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last) + SELECT SUM(word_count) FROM posts + WHERE user_id = :user_id + AND created_at BETWEEN :date_start AND :date_end + AND deleted_at IS NULL + SQL + + post_count = + DB.query_single(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last) + SELECT COUNT(*) FROM posts + WHERE user_id = :user_id + AND created_at BETWEEN :date_start AND :date_end + AND deleted_at IS NULL + SQL + + average_post_length = + post_count.first > 0 ? (total_words.first.to_f / post_count.first).round(2) : 0 + + # Calculated using the Flesch Reading Ease formula, + # with an approximation for syllables since this can + # be tricky to get right in SQL. + # + # Tries to handle short sentences or ones without delmiters + # and ending with emojis by treating them as a single sentence. + readability_score = + DB.query_single(<<~SQL, user_id: user.id, start: date.first, end: date.last) + WITH cleaned AS ( + SELECT + p.id AS post_id, + p.user_id, + p.created_at, + p.word_count, + regexp_replace(p.cooked, '<[^>]+>', ' ', 'g') AS plain + FROM posts p + WHERE p.user_id = :user_id + AND p.created_at BETWEEN :start AND :end + ), + metrics AS ( + SELECT + post_id, + user_id, + created_at, + plain, + word_count AS words, + regexp_count(plain, '[.!?;:](\s|$)') AS sentences_raw, + regexp_count(lower(plain), '[aeiouy]+') AS syllables + FROM cleaned + ), + scores AS ( + SELECT + post_id, + user_id, + created_at, + words, + syllables, + plain, + + CASE + WHEN sentences_raw = 0 AND words > 5 THEN 1 + ELSE sentences_raw + END AS sentences_fixed, + + -- Flesch Reading Ease formula + CASE + WHEN words = 0 THEN NULL + WHEN (CASE WHEN sentences_raw = 0 AND words > 5 THEN 1 ELSE sentences_raw END) = 0 THEN NULL + ELSE ( + 206.835 + - 1.015 * ( + words::float / + (CASE WHEN sentences_raw = 0 AND words > 5 THEN 1 ELSE sentences_raw END) + ) + - 84.6 * (syllables::float / words) + ) + END AS readability_score + + FROM metrics + ) + SELECT AVG(readability_score) AS avg_readability_score + FROM scores + GROUP BY user_id; + SQL + + { + data: { + total_words: total_words.first, + total_posts: post_count.first, + average_post_length: average_post_length, + readability_score: readability_score.first, + }, + identifier: "writing-analysis", + } + end + end + end +end diff --git a/app/services/discourse_rewind/fetch_reports.rb b/app/services/discourse_rewind/fetch_reports.rb index 3b28f68..eac34de 100644 --- a/app/services/discourse_rewind/fetch_reports.rb +++ b/app/services/discourse_rewind/fetch_reports.rb @@ -25,6 +25,7 @@ module DiscourseRewind REPORTS = [ Action::TopWords, Action::ReadingTime, + Action::WritingAnalysis, Action::Reactions, Action::Fbff, Action::MostViewedTags, diff --git a/assets/javascripts/discourse/components/reports/activity-calendar.gjs b/assets/javascripts/discourse/components/reports/activity-calendar.gjs index 8a8563c..6bb8c1b 100644 --- a/assets/javascripts/discourse/components/reports/activity-calendar.gjs +++ b/assets/javascripts/discourse/components/reports/activity-calendar.gjs @@ -57,18 +57,18 @@ export default class ActivityCalendar extends Component { @action computeClass(count) { if (!count) { - return "-empty"; + return "--empty"; } else if (count < 10) { - return "-low"; + return "--low"; } else if (count < 20) { - return "-medium"; + return "--medium"; } else { - return "-high"; + return "--high"; } }