Compare commits
4
Commits
async-reports
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5effd054ec | ||
|
|
d9f9e0c461 | ||
|
|
26a77d4c69 | ||
|
|
09ee91ad67 |
@@ -1,7 +1,5 @@
|
|||||||
# DiscourseRewind
|
# DiscourseRewind
|
||||||
|
|
||||||
Display stats on your last year of Discourse usage.
|
## DEPRECATED
|
||||||
|
|
||||||
## Usage
|
This plugin has been moved into Discourse core as at https://github.com/discourse/discourse/pull/36409/
|
||||||
|
|
||||||
Navigate to `/my/activity/rewind`
|
|
||||||
|
|||||||
@@ -6,7 +6,31 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class AiUsage < BaseReport
|
class AiUsage < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
total_requests: 247,
|
||||||
|
total_tokens: 156_890,
|
||||||
|
request_tokens: 45_230,
|
||||||
|
response_tokens: 111_660,
|
||||||
|
feature_usage: {
|
||||||
|
"chat_composer_helper" => 89,
|
||||||
|
"post_summarizer" => 56,
|
||||||
|
"semantic_search" => 42,
|
||||||
|
"topic_gist" => 38,
|
||||||
|
"similar_topics" => 22,
|
||||||
|
},
|
||||||
|
model_usage: {
|
||||||
|
"gpt-4" => 123,
|
||||||
|
"claude-3-5-sonnet" => 89,
|
||||||
|
"gpt-3.5-turbo" => 35,
|
||||||
|
},
|
||||||
|
success_rate: 94.7,
|
||||||
|
},
|
||||||
|
identifier: "ai-usage",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
return if !enabled?
|
return if !enabled?
|
||||||
|
|
||||||
base_query = AiApiRequestStat.where(user_id: user.id).where(bucket_date: date)
|
base_query = AiApiRequestStat.where(user_id: user.id).where(bucket_date: date)
|
||||||
|
|||||||
@@ -5,7 +5,19 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class Assignments < BaseReport
|
class Assignments < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
total_assigned: 24,
|
||||||
|
completed: 18,
|
||||||
|
pending: 6,
|
||||||
|
assigned_by_user: 15,
|
||||||
|
completion_rate: 75.0,
|
||||||
|
},
|
||||||
|
identifier: "assignments",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
return if !enabled?
|
return if !enabled?
|
||||||
|
|
||||||
# Assignments made to the user
|
# Assignments made to the user
|
||||||
|
|||||||
@@ -3,7 +3,37 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class BestPosts < BaseReport
|
class BestPosts < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
post_number: 5,
|
||||||
|
topic_id: 42,
|
||||||
|
like_count: 23,
|
||||||
|
reply_count: 8,
|
||||||
|
excerpt: "This is a great explanation of how ActiveRecord works under the hood...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
post_number: 12,
|
||||||
|
topic_id: 89,
|
||||||
|
like_count: 19,
|
||||||
|
reply_count: 5,
|
||||||
|
excerpt:
|
||||||
|
"Here's a comprehensive guide to testing Rails applications with RSpec and system tests...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
post_number: 3,
|
||||||
|
topic_id: 156,
|
||||||
|
like_count: 15,
|
||||||
|
reply_count: 12,
|
||||||
|
excerpt:
|
||||||
|
"The key to understanding PostgreSQL performance is looking at your query plans...",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
identifier: "best-posts",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
best_posts =
|
best_posts =
|
||||||
Post
|
Post
|
||||||
.where(user_id: user.id)
|
.where(user_id: user.id)
|
||||||
|
|||||||
@@ -3,7 +3,32 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class BestTopics < BaseReport
|
class BestTopics < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
topic_id: 1,
|
||||||
|
title: "How to get started with Rails",
|
||||||
|
excerpt: "A comprehensive guide to getting started with Ruby on Rails...",
|
||||||
|
yearly_score: 42.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic_id: 2,
|
||||||
|
title: "Best practices for database optimization",
|
||||||
|
excerpt: "Learn how to optimize your database queries for better performance...",
|
||||||
|
yearly_score: 38.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic_id: 3,
|
||||||
|
title: "Understanding ActiveRecord associations",
|
||||||
|
excerpt: "Deep dive into has_many, belongs_to, and other associations...",
|
||||||
|
yearly_score: 35.7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
identifier: "best-topics",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
best_topics =
|
best_topics =
|
||||||
TopTopic
|
TopTopic
|
||||||
.includes(:topic)
|
.includes(:topic)
|
||||||
|
|||||||
@@ -5,7 +5,27 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class ChatUsage < BaseReport
|
class ChatUsage < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
total_messages: 342,
|
||||||
|
favorite_channels: [
|
||||||
|
{ channel_id: 1, channel_name: "general", message_count: 156 },
|
||||||
|
{ channel_id: 2, channel_name: "tech-talk", message_count: 89 },
|
||||||
|
{ channel_id: 3, channel_name: "random", message_count: 45 },
|
||||||
|
{ channel_id: 4, channel_name: "dev", message_count: 32 },
|
||||||
|
{ channel_id: 5, channel_name: "announcements", message_count: 12 },
|
||||||
|
],
|
||||||
|
dm_message_count: 87,
|
||||||
|
unique_dm_channels: 12,
|
||||||
|
messages_with_reactions: 42,
|
||||||
|
total_reactions_received: 156,
|
||||||
|
avg_message_length: 78.5,
|
||||||
|
},
|
||||||
|
identifier: "chat-usage",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
return if !enabled?
|
return if !enabled?
|
||||||
|
|
||||||
messages =
|
messages =
|
||||||
|
|||||||
@@ -15,7 +15,47 @@ module DiscourseRewind
|
|||||||
}ix
|
}ix
|
||||||
MAX_RESULTS = 5
|
MAX_RESULTS = 5
|
||||||
|
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
favorite_gifs: [
|
||||||
|
{
|
||||||
|
url: "https://media.giphy.com/media/111ebonMs90YLu/giphy.gif",
|
||||||
|
usage_count: 12,
|
||||||
|
likes: 45,
|
||||||
|
reactions: 23,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif",
|
||||||
|
usage_count: 8,
|
||||||
|
likes: 32,
|
||||||
|
reactions: 18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://media1.tenor.com/m/XnODae53zvYAAAAd/joke-stickfigure.gif",
|
||||||
|
usage_count: 7,
|
||||||
|
likes: 28,
|
||||||
|
reactions: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://c.tenor.com/tX_T48A14BwAAAAd/khaby-really.gif",
|
||||||
|
usage_count: 5,
|
||||||
|
likes: 20,
|
||||||
|
reactions: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif",
|
||||||
|
usage_count: 4,
|
||||||
|
likes: 15,
|
||||||
|
reactions: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total_gif_usage: 36,
|
||||||
|
},
|
||||||
|
identifier: "favorite-gifs",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
gif_data = {}
|
gif_data = {}
|
||||||
|
|
||||||
# Get GIFs from posts
|
# Get GIFs from posts
|
||||||
|
|||||||
@@ -9,7 +9,26 @@ module DiscourseRewind
|
|||||||
LIKE_SCORE = 1
|
LIKE_SCORE = 1
|
||||||
REPLY_SCORE = 10
|
REPLY_SCORE = 10
|
||||||
|
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
fbff: {
|
||||||
|
id: 2,
|
||||||
|
username: "codingpal",
|
||||||
|
name: "Coding Pal",
|
||||||
|
avatar_template: "/letter_avatar_proxy/v4/letter/c/3be4f8/{size}.png",
|
||||||
|
},
|
||||||
|
yourself: {
|
||||||
|
id: 1,
|
||||||
|
username: "you",
|
||||||
|
name: "You",
|
||||||
|
avatar_template: "/letter_avatar_proxy/v4/letter/y/f05b48/{size}.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
identifier: "fbff",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
most_liked_users =
|
most_liked_users =
|
||||||
like_query(date)
|
like_query(date)
|
||||||
.where(acting_user_id: user.id)
|
.where(acting_user_id: user.id)
|
||||||
|
|||||||
@@ -5,7 +5,27 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class Invites < BaseReport
|
class Invites < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
total_invites: 18,
|
||||||
|
redeemed_count: 12,
|
||||||
|
redemption_rate: 66.7,
|
||||||
|
invitee_post_count: 145,
|
||||||
|
invitee_topic_count: 23,
|
||||||
|
invitee_like_count: 89,
|
||||||
|
avg_trust_level: 1.8,
|
||||||
|
most_active_invitee: {
|
||||||
|
id: 42,
|
||||||
|
username: "newbie_123",
|
||||||
|
name: "New User",
|
||||||
|
avatar_template: "/letter_avatar_proxy/v4/letter/n/8c91d9/{size}.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
identifier: "invites",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
# Get all invites created by this user in the date range
|
# Get all invites created by this user in the date range
|
||||||
invites = Invite.where(invited_by_id: user.id).where(created_at: date)
|
invites = Invite.where(invited_by_id: user.id).where(created_at: date)
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ module DiscourseRewind
|
|||||||
TopicViewItem
|
TopicViewItem
|
||||||
.joins(:topic)
|
.joins(:topic)
|
||||||
.joins("INNER JOIN categories ON categories.id = topics.category_id")
|
.joins("INNER JOIN categories ON categories.id = topics.category_id")
|
||||||
.where(user: user)
|
.where(
|
||||||
.where(viewed_at: date)
|
user: user,
|
||||||
.where(categories: { id: user.guardian.allowed_category_ids })
|
viewed_at: date,
|
||||||
|
categories: {
|
||||||
|
id: user.guardian.allowed_category_ids,
|
||||||
|
},
|
||||||
|
)
|
||||||
.group("categories.id, categories.name")
|
.group("categories.id, categories.name")
|
||||||
.order("COUNT(*) DESC")
|
.order("COUNT(*) DESC")
|
||||||
.limit(4)
|
.limit(4)
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ module DiscourseRewind
|
|||||||
.joins(:topic)
|
.joins(:topic)
|
||||||
.joins("INNER JOIN topic_tags ON topic_tags.topic_id = topics.id")
|
.joins("INNER JOIN topic_tags ON topic_tags.topic_id = topics.id")
|
||||||
.joins("INNER JOIN tags ON tags.id = topic_tags.tag_id")
|
.joins("INNER JOIN tags ON tags.id = topic_tags.tag_id")
|
||||||
.where(user: user)
|
.where(user: user, viewed_at: date, tags: { id: Tag.visible(user.guardian).pluck(:id) })
|
||||||
.where(viewed_at: date)
|
|
||||||
.where(tags: { id: Tag.visible(user.guardian).pluck(:id) })
|
|
||||||
.group("tags.id, tags.name")
|
.group("tags.id, tags.name")
|
||||||
.order("COUNT(DISTINCT topic_views.topic_id) DESC")
|
.order("COUNT(DISTINCT topic_views.topic_id) DESC")
|
||||||
.limit(4)
|
.limit(4)
|
||||||
|
|||||||
@@ -5,7 +5,21 @@
|
|||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
module Action
|
module Action
|
||||||
class NewUserInteractions < BaseReport
|
class NewUserInteractions < BaseReport
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
total_interactions: 127,
|
||||||
|
likes_given: 45,
|
||||||
|
replies_to_new_users: 62,
|
||||||
|
mentions_to_new_users: 20,
|
||||||
|
topics_with_new_users: 8,
|
||||||
|
unique_new_users: 24,
|
||||||
|
new_users_count: 156,
|
||||||
|
},
|
||||||
|
identifier: "new-user-interactions",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
year_start = Date.new(date.first.year, 1, 1)
|
year_start = Date.new(date.first.year, 1, 1)
|
||||||
|
|
||||||
# Find users who created accounts this year
|
# Find users who created accounts this year
|
||||||
@@ -19,66 +33,45 @@ module DiscourseRewind
|
|||||||
return if new_user_ids.empty?
|
return if new_user_ids.empty?
|
||||||
|
|
||||||
# Count likes given to new users
|
# Count likes given to new users
|
||||||
likes_scope =
|
liked_user_ids =
|
||||||
UserAction.where(
|
UserAction
|
||||||
|
.where(
|
||||||
acting_user_id: user.id,
|
acting_user_id: user.id,
|
||||||
user_id: new_user_ids,
|
user_id: new_user_ids,
|
||||||
action_type: UserAction::WAS_LIKED,
|
action_type: UserAction::WAS_LIKED,
|
||||||
).where(created_at: date)
|
)
|
||||||
likes_given = likes_scope.count
|
.where(created_at: date)
|
||||||
liked_user_ids = likes_scope.distinct.pluck(:user_id)
|
.distinct
|
||||||
|
.pluck(:user_id)
|
||||||
|
|
||||||
# Count replies to new users' posts
|
# Count replies to new users' posts
|
||||||
replies_scope =
|
replied_user_ids =
|
||||||
Post
|
Post
|
||||||
.joins(
|
.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",
|
"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(posts: { user_id: user.id, deleted_at: nil, created_at: date })
|
||||||
.where("parent_posts.user_id": new_user_ids)
|
.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
|
.distinct
|
||||||
.count
|
.pluck("parent_posts.user_id")
|
||||||
|
|
||||||
# Count direct messages/mentions to new users
|
# Count direct mentions to new users
|
||||||
mentions_scope =
|
mentioned_user_ids =
|
||||||
Post
|
Post
|
||||||
.joins(
|
.joins(
|
||||||
"INNER JOIN user_actions ON user_actions.target_post_id = posts.id AND user_actions.action_type = #{UserAction::MENTION}",
|
"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(posts: { user_id: user.id, deleted_at: nil, created_at: date })
|
||||||
.where(user_actions: { user_id: new_user_ids })
|
.where(user_actions: { user_id: new_user_ids })
|
||||||
mentions_to_new_users = mentions_scope.distinct.count
|
.distinct
|
||||||
mentioned_user_ids = mentions_scope.distinct.pluck("user_actions.user_id")
|
.pluck("user_actions.user_id")
|
||||||
|
|
||||||
# Unique new users interacted with
|
# Unique new users interacted with
|
||||||
unique_new_users = (liked_user_ids + replied_user_ids + mentioned_user_ids).uniq.count
|
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: { unique_new_users: unique_new_users }, identifier: "new-user-interactions" }
|
||||||
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ module DiscourseRewind
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sort_and_limit(reactions)
|
def sort_and_limit(reactions)
|
||||||
reactions.sort_by { |_, v| -v }.first(5).reverse.to_h
|
reactions.sort_by { |_, value| -value }.take(5).reverse.to_h
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,46 @@ module DiscourseRewind
|
|||||||
NIGHT_OWL_THRESHOLD_PM = 22..23
|
NIGHT_OWL_THRESHOLD_PM = 22..23
|
||||||
NIGHT_OWL_THRESHOLD_AM = 0..2
|
NIGHT_OWL_THRESHOLD_AM = 0..2
|
||||||
|
|
||||||
|
FakeData = {
|
||||||
|
data: {
|
||||||
|
activity_by_hour: {
|
||||||
|
0 => 12,
|
||||||
|
1 => 8,
|
||||||
|
2 => 5,
|
||||||
|
3 => 2,
|
||||||
|
4 => 1,
|
||||||
|
5 => 3,
|
||||||
|
6 => 8,
|
||||||
|
7 => 15,
|
||||||
|
8 => 25,
|
||||||
|
9 => 32,
|
||||||
|
10 => 28,
|
||||||
|
11 => 24,
|
||||||
|
12 => 22,
|
||||||
|
13 => 20,
|
||||||
|
14 => 26,
|
||||||
|
15 => 30,
|
||||||
|
16 => 28,
|
||||||
|
17 => 22,
|
||||||
|
18 => 18,
|
||||||
|
19 => 16,
|
||||||
|
20 => 14,
|
||||||
|
21 => 18,
|
||||||
|
22 => 22,
|
||||||
|
23 => 15,
|
||||||
|
},
|
||||||
|
most_active_hour: 9,
|
||||||
|
personality: {
|
||||||
|
type: "early_bird",
|
||||||
|
percentage: 28.5,
|
||||||
|
},
|
||||||
|
total_activities: 414,
|
||||||
|
},
|
||||||
|
identifier: "time-of-day-activity",
|
||||||
|
}
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
return FakeData if Rails.env.development?
|
||||||
# Get activity by hour of day (in user's timezone)
|
# Get activity by hour of day (in user's timezone)
|
||||||
activity_by_hour = get_activity_by_hour
|
activity_by_hour = get_activity_by_hour
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module DiscourseRewind
|
|||||||
{ word: "you", score: 80 },
|
{ word: "you", score: 80 },
|
||||||
{ word: "overachieved", score: 70 },
|
{ word: "overachieved", score: 70 },
|
||||||
{ word: "assume", score: 60 },
|
{ word: "assume", score: 60 },
|
||||||
{ word: "week", score: 50 },
|
|
||||||
],
|
],
|
||||||
identifier: "top-words",
|
identifier: "top-words",
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,13 @@ module DiscourseRewind
|
|||||||
LIMIT 100
|
LIMIT 100
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
word_score = words.map { { word: _1.original_word, score: _1.ndoc + _1.nentry } }
|
word_score =
|
||||||
|
words
|
||||||
|
.map do |word_data|
|
||||||
|
{ word: word_data.original_word, score: word_data.ndoc + word_data.nentry }
|
||||||
|
end
|
||||||
|
.sort_by! { |w| -w[:score] }
|
||||||
|
.take(5)
|
||||||
|
|
||||||
{ data: word_score, identifier: "top-words" }
|
{ data: word_score, identifier: "top-words" }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -5,7 +5,8 @@ module DiscourseRewind
|
|||||||
#
|
#
|
||||||
# @example
|
# @example
|
||||||
# ::DiscourseRewind::Rewind::Fetch.call(
|
# ::DiscourseRewind::Rewind::Fetch.call(
|
||||||
# guardian: guardian
|
# guardian: guardian,
|
||||||
|
# params: { year: 2023, username: 'codinghorror' }
|
||||||
# )
|
# )
|
||||||
#
|
#
|
||||||
class FetchReports
|
class FetchReports
|
||||||
@@ -24,6 +25,7 @@ module DiscourseRewind
|
|||||||
REPORTS = [
|
REPORTS = [
|
||||||
Action::TopWords,
|
Action::TopWords,
|
||||||
Action::ReadingTime,
|
Action::ReadingTime,
|
||||||
|
Action::WritingAnalysis,
|
||||||
Action::Reactions,
|
Action::Reactions,
|
||||||
Action::Fbff,
|
Action::Fbff,
|
||||||
Action::MostViewedTags,
|
Action::MostViewedTags,
|
||||||
|
|||||||
@@ -28,21 +28,47 @@ export default class ActivityCalendar extends Component {
|
|||||||
return moment().month(monthIndex).format("MMM");
|
return moment().month(monthIndex).format("MMM");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
computeCellTitle(cell) {
|
||||||
|
if (!cell || !cell.date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = moment(cell.date).format("LL");
|
||||||
|
|
||||||
|
if (cell.visited && cell.post_count === 0) {
|
||||||
|
return i18n(
|
||||||
|
"discourse_rewind.reports.activity_calendar.cell_title.visited_no_posts",
|
||||||
|
{ date }
|
||||||
|
);
|
||||||
|
} else if (cell.post_count > 0) {
|
||||||
|
return i18n(
|
||||||
|
"discourse_rewind.reports.activity_calendar.cell_title.visited_with_posts",
|
||||||
|
{ date, count: cell.post_count }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n(
|
||||||
|
"discourse_rewind.reports.activity_calendar.cell_title.no_activity",
|
||||||
|
{ date }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
computeClass(count) {
|
computeClass(count) {
|
||||||
if (!count) {
|
if (!count) {
|
||||||
return "-empty";
|
return "--empty";
|
||||||
} else if (count < 10) {
|
} else if (count < 10) {
|
||||||
return "-low";
|
return "--low";
|
||||||
} else if (count < 20) {
|
} else if (count < 20) {
|
||||||
return "-medium";
|
return "--medium";
|
||||||
} else {
|
} else {
|
||||||
return "-high";
|
return "--high";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rewind-report-page -activity-calendar">
|
<div class="rewind-report-page --activity-calendar">
|
||||||
<h2 class="rewind-report-title">{{i18n
|
<h2 class="rewind-report-title">{{i18n
|
||||||
"discourse_rewind.reports.activity_calendar.title"
|
"discourse_rewind.reports.activity_calendar.title"
|
||||||
}}</h2>
|
}}</h2>
|
||||||
@@ -107,7 +133,7 @@ export default class ActivityCalendar extends Component {
|
|||||||
{{#each row as |cell|}}
|
{{#each row as |cell|}}
|
||||||
<td
|
<td
|
||||||
data-date={{cell.date}}
|
data-date={{cell.date}}
|
||||||
title={{cell.date}}
|
title={{this.computeCellTitle cell}}
|
||||||
class={{concatClass
|
class={{concatClass
|
||||||
"rewind-calendar-cell"
|
"rewind-calendar-cell"
|
||||||
(this.computeClass cell.post_count)
|
(this.computeClass cell.post_count)
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { get } from "@ember/helper";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
|
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class AiUsage extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
matrixInterval = null;
|
||||||
|
|
||||||
|
get totalRequests() {
|
||||||
|
return this.args.report.data.total_requests ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalTokens() {
|
||||||
|
return this.args.report.data.total_tokens ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get successRate() {
|
||||||
|
return this.args.report.data.success_rate ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get featureUsage() {
|
||||||
|
return Object.entries(this.args.report.data.feature_usage ?? {})
|
||||||
|
.filter(([name]) => name && name.trim().length > 0)
|
||||||
|
.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
get modelUsage() {
|
||||||
|
return Object.entries(this.args.report.data.model_usage ?? {})
|
||||||
|
.filter(([name]) => name && name.trim().length > 0)
|
||||||
|
.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
formatFeatureName(featureName) {
|
||||||
|
return featureName.replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setupMatrix(element) {
|
||||||
|
const canvas = element;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
canvas.width = element.offsetWidth;
|
||||||
|
canvas.height = element.offsetHeight;
|
||||||
|
|
||||||
|
const characters =
|
||||||
|
"01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン";
|
||||||
|
const fontSize = 14;
|
||||||
|
const columns = Math.floor(canvas.width / fontSize);
|
||||||
|
const drops = Array(columns).fill(1);
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
ctx.fillStyle = "#0f0";
|
||||||
|
ctx.font = `${fontSize}px monospace`;
|
||||||
|
|
||||||
|
for (let i = 0; i < drops.length; i++) {
|
||||||
|
const char = characters[Math.floor(Math.random() * characters.length)];
|
||||||
|
const x = i * fontSize;
|
||||||
|
const y = drops[i] * fontSize;
|
||||||
|
|
||||||
|
ctx.fillText(char, x, y);
|
||||||
|
|
||||||
|
if (y > canvas.height && Math.random() > 0.975) {
|
||||||
|
drops[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
drops[i]++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.matrixInterval = setInterval(draw, 33);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
cleanupMatrix() {
|
||||||
|
if (this.matrixInterval) {
|
||||||
|
clearInterval(this.matrixInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rewind-report-page --ai-usage">
|
||||||
|
<div class="matrix-container">
|
||||||
|
<canvas
|
||||||
|
class="matrix-rain"
|
||||||
|
{{didInsert this.setupMatrix}}
|
||||||
|
{{willDestroy this.cleanupMatrix}}
|
||||||
|
></canvas>
|
||||||
|
|
||||||
|
<div class="matrix-content">
|
||||||
|
<h2 class="matrix-title">
|
||||||
|
<div class="matrix-subhead">
|
||||||
|
{{i18n
|
||||||
|
"discourse_rewind.reports.ai_usage.wake_up"
|
||||||
|
username=this.currentUser.username
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{i18n "discourse_rewind.reports.ai_usage.system_title"}}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="matrix-stats">
|
||||||
|
<div class="matrix-stat">
|
||||||
|
<div class="matrix-stat__label">
|
||||||
|
{{i18n "discourse_rewind.reports.ai_usage.total_requests"}}
|
||||||
|
</div>
|
||||||
|
<div class="matrix-stat__value">
|
||||||
|
{{number this.totalRequests}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="matrix-stat">
|
||||||
|
<div class="matrix-stat__label">
|
||||||
|
{{i18n "discourse_rewind.reports.ai_usage.total_tokens"}}
|
||||||
|
</div>
|
||||||
|
<div class="matrix-stat__value">{{number this.totalTokens}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="matrix-stat">
|
||||||
|
<div class="matrix-stat__label">
|
||||||
|
{{i18n "discourse_rewind.reports.ai_usage.success_rate"}}
|
||||||
|
</div>
|
||||||
|
<div class="matrix-stat__value">
|
||||||
|
<span class="number">
|
||||||
|
{{this.successRate}}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.featureUsage.length}}
|
||||||
|
<div class="matrix-section">
|
||||||
|
<div class="matrix-section__title">>
|
||||||
|
{{i18n "discourse_rewind.reports.ai_usage.section_features"}}
|
||||||
|
</div>
|
||||||
|
<div class="matrix-list">
|
||||||
|
{{#each this.featureUsage as |entry|}}
|
||||||
|
<div class="matrix-list__item">
|
||||||
|
<span class="matrix-list__name">
|
||||||
|
{{this.formatFeatureName (get entry "0")}}
|
||||||
|
</span>
|
||||||
|
<span class="matrix-list__count">{{get entry "1"}}</span>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.modelUsage.length}}
|
||||||
|
<div class="matrix-section">
|
||||||
|
<div class="matrix-section__title">>
|
||||||
|
{{i18n "discourse_rewind.reports.ai_usage.section_models"}}
|
||||||
|
</div>
|
||||||
|
<div class="matrix-list">
|
||||||
|
{{#each this.modelUsage as |entry|}}
|
||||||
|
<div class="matrix-list__item">
|
||||||
|
<span class="matrix-list__name">{{get entry "0"}}</span>
|
||||||
|
<span class="matrix-list__count">{{get entry "1"}}</span>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
const Assignments = <template>
|
||||||
|
<div class="rewind-report-page --assignments">
|
||||||
|
<div class="sticky-board">
|
||||||
|
<div class="sticky-note --yellow --rotate-left">
|
||||||
|
<div class="sticky-note__content">
|
||||||
|
<div class="sticky-note__title">
|
||||||
|
{{i18n "discourse_rewind.reports.assignments.completed"}}
|
||||||
|
</div>
|
||||||
|
<div class="sticky-note__value">
|
||||||
|
{{number @report.data.completed}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sticky-note --pink --rotate-right">
|
||||||
|
<div class="sticky-note__content">
|
||||||
|
<div class="sticky-note__title">
|
||||||
|
{{i18n "discourse_rewind.reports.assignments.pending"}}
|
||||||
|
</div>
|
||||||
|
<div class="sticky-note__value">{{number @report.data.pending}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sticky-note --blue --rotate-left-small">
|
||||||
|
<div class="sticky-note__content">
|
||||||
|
<div class="sticky-note__title">
|
||||||
|
{{i18n "discourse_rewind.reports.assignments.total_assigned"}}
|
||||||
|
</div>
|
||||||
|
<div class="sticky-note__value">
|
||||||
|
{{number @report.data.total_assigned}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sticky-note --green --rotate-right-small">
|
||||||
|
<div class="sticky-note__content">
|
||||||
|
<div class="sticky-note__title">
|
||||||
|
{{i18n "discourse_rewind.reports.assignments.assigned_by_user"}}
|
||||||
|
</div>
|
||||||
|
<div class="sticky-note__value">
|
||||||
|
{{number @report.data.assigned_by_user}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sticky-note --orange">
|
||||||
|
<div class="sticky-note__content">
|
||||||
|
<div class="sticky-note__title">
|
||||||
|
{{i18n "discourse_rewind.reports.assignments.completion_rate"}}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="sticky-note__value"
|
||||||
|
>{{@report.data.completion_rate}}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default Assignments;
|
||||||
@@ -1,37 +1,41 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { concat } from "@ember/helper";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import icon from "discourse/helpers/d-icon";
|
import icon from "discourse/helpers/d-icon";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
export default class BestPosts extends Component {
|
export default class BestPosts extends Component {
|
||||||
rank(idx) {
|
rankClass(idx) {
|
||||||
return idx + 1;
|
return `rank-${idx + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if @report.data.length}}
|
{{#if @report.data.length}}
|
||||||
<div class="rewind-report-page -best-posts">
|
<div class="rewind-report-page --best-posts">
|
||||||
<h2 class="rewind-report-title">{{i18n
|
<h2 class="rewind-report-title">
|
||||||
|
{{i18n
|
||||||
"discourse_rewind.reports.best_posts.title"
|
"discourse_rewind.reports.best_posts.title"
|
||||||
[email protected]
|
[email protected]
|
||||||
}}</h2>
|
}}
|
||||||
|
</h2>
|
||||||
<div class="rewind-report-container">
|
<div class="rewind-report-container">
|
||||||
{{#each @report.data as |post idx|}}
|
{{#each @report.data as |post idx|}}
|
||||||
<div class={{concat "rewind-card" " rank-" (this.rank idx)}}>
|
<div class={{concatClass "rewind-card" (this.rankClass idx)}}>
|
||||||
<span class="best-posts -rank"></span>
|
<span class="best-posts --rank"></span>
|
||||||
<span class="best-posts -rank"></span>
|
<span class="best-posts --rank"></span>
|
||||||
<div class="best-posts__post"><p>{{htmlSafe
|
<div class="best-posts__post">
|
||||||
post.excerpt
|
<p>{{htmlSafe post.excerpt}}</p>
|
||||||
}}</p></div>
|
</div>
|
||||||
<div class="best-posts__metadata">
|
<div class="best-posts__metadata">
|
||||||
<span class="best-posts__likes">
|
<span class="best-posts__likes">
|
||||||
{{icon "heart"}}{{post.like_count}}</span>
|
{{icon "heart"}}{{post.like_count}}
|
||||||
|
</span>
|
||||||
<span class="best-posts__replies">
|
<span class="best-posts__replies">
|
||||||
{{icon "comment"}}{{post.reply_count}}</span>
|
{{icon "comment"}}{{post.reply_count}}
|
||||||
<a href="/t/{{post.topic_id}}/{{post.post_number}}">{{i18n
|
</span>
|
||||||
"discourse_rewind.reports.best_posts.view_post"
|
<a href="/t/{{post.topic_id}}/{{post.post_number}}">
|
||||||
}}</a>
|
{{i18n "discourse_rewind.reports.best_posts.view_post"}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { concat } from "@ember/helper";
|
import { concat } from "@ember/helper";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||||
|
import getURL from "discourse/lib/get-url";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
export default class BestTopics extends Component {
|
export default class BestTopics extends Component {
|
||||||
rank(idx) {
|
rankClass(idx) {
|
||||||
return idx + 1;
|
return `rank-${idx + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if @report.data.length}}
|
{{#if @report.data.length}}
|
||||||
<div class="rewind-report-page -best-topics">
|
<div class="rewind-report-page --best-topics">
|
||||||
<h2 class="rewind-report-title">{{i18n
|
<h2 class="rewind-report-title">
|
||||||
|
{{i18n
|
||||||
"discourse_rewind.reports.best_topics.title"
|
"discourse_rewind.reports.best_topics.title"
|
||||||
[email protected]
|
[email protected]
|
||||||
}}</h2>
|
}}
|
||||||
|
</h2>
|
||||||
<div class="rewind-report-container">
|
<div class="rewind-report-container">
|
||||||
<div class="rewind-card">
|
<div class="rewind-card">
|
||||||
{{#each @report.data as |topic idx|}}
|
{{#each @report.data as |topic idx|}}
|
||||||
<a
|
<a
|
||||||
href={{concat "/t/-/" topic.topic_id}}
|
href={{getURL (concat "/t/-/" topic.topic_id)}}
|
||||||
class={{concat "best-topics__topic" " rank-" (this.rank idx)}}
|
class={{concatClass "best-topics__topic" (this.rankClass idx)}}
|
||||||
>
|
>
|
||||||
<span class="best-topics -rank"></span>
|
<span class="best-topics --rank"></span>
|
||||||
<span class="best-topics -rank"></span>
|
<span class="best-topics --rank"></span>
|
||||||
<h2 class="best-topics__header">{{topic.title}}</h2>
|
<h2 class="best-topics__header">{{topic.title}}</h2>
|
||||||
<span class="best-topics__excerpt">{{replaceEmoji
|
<span class="best-topics__excerpt">
|
||||||
(htmlSafe topic.excerpt)
|
{{replaceEmoji (htmlSafe topic.excerpt)}}
|
||||||
}}</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { concat } from "@ember/helper";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import avatar from "discourse/helpers/avatar";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
const BotMessage = <template>
|
||||||
|
<div class="chat-message__avatar">🤖</div>
|
||||||
|
<div class="chat-message__bubble">
|
||||||
|
<div class="chat-message__author">
|
||||||
|
{{i18n "discourse_rewind.reports.chat_usage.bot_name"}}
|
||||||
|
</div>
|
||||||
|
{{#if @message}}
|
||||||
|
<div class="chat-message__text">
|
||||||
|
{{htmlSafe @message}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{yield}}
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
const UserMessage = <template>
|
||||||
|
<div class="chat-message__bubble">
|
||||||
|
<div class="chat-message__author">
|
||||||
|
{{i18n "discourse_rewind.reports.chat_usage.you"}}
|
||||||
|
</div>
|
||||||
|
{{#if @replyKey}}
|
||||||
|
<div class="chat-message__text">
|
||||||
|
{{i18n (concat "discourse_rewind.reports.chat_usage." @replyKey)}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{yield}}
|
||||||
|
</div>
|
||||||
|
<div class="chat-message__avatar">
|
||||||
|
{{avatar @user imageSize="small"}}
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default class ChatUsage extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
get favoriteChannels() {
|
||||||
|
return this.args.report.data.favorite_channels ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rewind-report-page --chat-usage">
|
||||||
|
<h2 class="rewind-report-title">{{i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.title"
|
||||||
|
}}</h2>
|
||||||
|
|
||||||
|
<div class="chat-window">
|
||||||
|
<div class="chat-window__header">
|
||||||
|
<span class="chat-window__title">
|
||||||
|
{{i18n "discourse_rewind.reports.chat_usage.channel_title"}}
|
||||||
|
</span>
|
||||||
|
<span class="chat-window__status">
|
||||||
|
{{i18n "discourse_rewind.reports.chat_usage.status_online"}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-window__messages">
|
||||||
|
<div class="chat-message --left">
|
||||||
|
<BotMessage
|
||||||
|
@message={{i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.message_1"
|
||||||
|
count=(number @report.data.total_messages)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message --right">
|
||||||
|
<UserMessage @user={{this.currentUser}} @replyKey="reply_1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message --left">
|
||||||
|
<BotMessage
|
||||||
|
@message={{htmlSafe
|
||||||
|
(i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.message_2"
|
||||||
|
dm_count=(number @report.data.dm_message_count)
|
||||||
|
channel_count=(number @report.data.unique_dm_channels)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message --right">
|
||||||
|
<UserMessage @user={{this.currentUser}} @replyKey="reply_2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message --left">
|
||||||
|
<BotMessage
|
||||||
|
@message={{htmlSafe
|
||||||
|
(i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.message_3"
|
||||||
|
count=(number @report.data.total_reactions_received)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message --right">
|
||||||
|
<UserMessage @user={{this.currentUser}} @replyKey="reply_3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message --left">
|
||||||
|
<BotMessage
|
||||||
|
@message={{htmlSafe
|
||||||
|
(i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.message_4"
|
||||||
|
[email protected]_message_length
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.favoriteChannels.length}}
|
||||||
|
<div class="chat-message --left">
|
||||||
|
<BotMessage
|
||||||
|
@message={{i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.message_5"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="chat-message__channels">
|
||||||
|
{{#each this.favoriteChannels as |channel|}}
|
||||||
|
<a
|
||||||
|
class="chat-channel-link"
|
||||||
|
href={{concat "/chat/c/-/" channel.channel_id}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="chat-channel-link__name"
|
||||||
|
>#{{channel.channel_name}}</span>
|
||||||
|
<span class="chat-channel-link__count">
|
||||||
|
{{number channel.message_count}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</BotMessage>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="chat-message --right">
|
||||||
|
<UserMessage @user={{this.currentUser}}>
|
||||||
|
<img
|
||||||
|
src="/plugins/discourse-rewind/images/dancing_baby.gif"
|
||||||
|
alt={{i18n
|
||||||
|
"discourse_rewind.reports.chat_usage.dancing_baby_alt"
|
||||||
|
}}
|
||||||
|
class="chat-message__gif"
|
||||||
|
/>
|
||||||
|
</UserMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import icon from "discourse/helpers/d-icon";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class FavoriteGifs extends Component {
|
||||||
|
get favoriteGifs() {
|
||||||
|
return this.args.report.data.favorite_gifs ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.favoriteGifs.length}}
|
||||||
|
<div class="rewind-report-page --favorite-gifs">
|
||||||
|
<h2 class="rewind-report-title">{{i18n
|
||||||
|
"discourse_rewind.reports.favorite_gifs.title"
|
||||||
|
count=this.favoriteGifs.length
|
||||||
|
}}</h2>
|
||||||
|
<div class="rewind-report-subtitle">{{i18n
|
||||||
|
"discourse_rewind.reports.favorite_gifs.total_usage"
|
||||||
|
[email protected]_gif_usage
|
||||||
|
}}</div>
|
||||||
|
<div class="rewind-report-container">
|
||||||
|
{{#each this.favoriteGifs as |gif idx|}}
|
||||||
|
<div class="rewind-card scale">
|
||||||
|
<div class="favorite-gifs__gif">
|
||||||
|
<img
|
||||||
|
src={{gif.url}}
|
||||||
|
alt="GIF #{{idx}}"
|
||||||
|
class="favorite-gifs__image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div class="favorite-gifs__stats">
|
||||||
|
<span class="favorite-gifs__stat">
|
||||||
|
{{icon "repeat"}}
|
||||||
|
{{number gif.usage_count}}
|
||||||
|
</span>
|
||||||
|
<span class="favorite-gifs__stat">
|
||||||
|
{{icon "heart"}}
|
||||||
|
{{number gif.likes}}
|
||||||
|
</span>
|
||||||
|
<span class="favorite-gifs__stat">
|
||||||
|
{{icon "smile"}}
|
||||||
|
{{number gif.reactions}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -3,10 +3,10 @@ import avatar from "discourse/helpers/bound-avatar-template";
|
|||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
const FBFF = <template>
|
const FBFF = <template>
|
||||||
<div class="rewind-report-page -fbff">
|
<div class="rewind-report-page --fbff">
|
||||||
<h2 class="rewind-report-title">{{i18n
|
<h2 class="rewind-report-title">
|
||||||
"discourse_rewind.reports.fbff.title"
|
{{i18n "discourse_rewind.reports.fbff.title"}}
|
||||||
}}</h2>
|
</h2>
|
||||||
<div class="rewind-report-container">
|
<div class="rewind-report-container">
|
||||||
<div class="rewind-card">
|
<div class="rewind-card">
|
||||||
<div class="fbff-avatar-container">
|
<div class="fbff-avatar-container">
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { concat } from "@ember/helper";
|
||||||
|
import avatar from "discourse/helpers/bound-avatar-template";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class Invites extends Component {
|
||||||
|
get mostActiveInvitee() {
|
||||||
|
return this.args.report.data.most_active_invitee;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rewind-report-page --invites">
|
||||||
|
<div class="guest-book">
|
||||||
|
<div class="guest-book__cover">
|
||||||
|
<div class="guest-book__title">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.guest_book_title"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__subtitle">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.guest_book_subtitle"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__page">
|
||||||
|
<div class="guest-book__entry">
|
||||||
|
<div class="guest-book__entry-label">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.label_invitations_sent"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__entry-value">
|
||||||
|
{{number @report.data.total_invites}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__entry">
|
||||||
|
<div class="guest-book__entry-label">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.label_guests_joined"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__entry-value">
|
||||||
|
{{number @report.data.redeemed_count}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__entry">
|
||||||
|
<div class="guest-book__entry-label">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.label_acceptance_rate"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__entry-value">
|
||||||
|
{{@report.data.redemption_rate}}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__divider"></div>
|
||||||
|
|
||||||
|
<div class="guest-book__section-title">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.section_contributions"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__entry --small">
|
||||||
|
<div class="guest-book__entry-label">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.label_posts_written"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__entry-value">
|
||||||
|
{{number @report.data.invitee_post_count}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__entry --small">
|
||||||
|
<div class="guest-book__entry-label">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.label_topics_started"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__entry-value">
|
||||||
|
{{number @report.data.invitee_topic_count}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guest-book__entry --small">
|
||||||
|
<div class="guest-book__entry-label">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.label_likes_given"}}
|
||||||
|
</div>
|
||||||
|
<div class="guest-book__entry-value">
|
||||||
|
{{number @report.data.invitee_like_count}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.mostActiveInvitee}}
|
||||||
|
<div class="guest-book__divider"></div>
|
||||||
|
|
||||||
|
<div class="guest-book__section-title">
|
||||||
|
{{i18n "discourse_rewind.reports.invites.section_most_active"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={{concat "/u/" this.mostActiveInvitee.username}}
|
||||||
|
class="guest-book__signature"
|
||||||
|
>
|
||||||
|
{{avatar
|
||||||
|
this.mostActiveInvitee.avatar_template
|
||||||
|
"large"
|
||||||
|
username=this.mostActiveInvitee.username
|
||||||
|
name=this.mostActiveInvitee.name
|
||||||
|
}}
|
||||||
|
<div class="guest-book__signature-info">
|
||||||
|
<span class="guest-book__signature-name">
|
||||||
|
{{this.mostActiveInvitee.username}}
|
||||||
|
</span>
|
||||||
|
{{#if this.mostActiveInvitee.name}}
|
||||||
|
<span class="guest-book__signature-realname">
|
||||||
|
{{this.mostActiveInvitee.name}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ import { i18n } from "discourse-i18n";
|
|||||||
|
|
||||||
const MostViewedCategories = <template>
|
const MostViewedCategories = <template>
|
||||||
{{#if @report.data.length}}
|
{{#if @report.data.length}}
|
||||||
<div class="rewind-report-page -most-viewed-categories">
|
<div class="rewind-report-page --most-viewed-categories">
|
||||||
<h2 class="rewind-report-title">{{i18n
|
<h2 class="rewind-report-title">
|
||||||
|
{{i18n
|
||||||
"discourse_rewind.reports.most_viewed_categories.title"
|
"discourse_rewind.reports.most_viewed_categories.title"
|
||||||
[email protected]
|
[email protected]
|
||||||
}}</h2>
|
}}
|
||||||
|
</h2>
|
||||||
<div class="rewind-report-container">
|
<div class="rewind-report-container">
|
||||||
{{#each @report.data as |data|}}
|
{{#each @report.data as |data|}}
|
||||||
<a class="folder-wrapper" href={{concat "/c/-/" data.category_id}}>
|
<a class="folder-wrapper" href={{concat "/c/-/" data.category_id}}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { i18n } from "discourse-i18n";
|
|||||||
|
|
||||||
const MostViewedTags = <template>
|
const MostViewedTags = <template>
|
||||||
{{#if @report.data.length}}
|
{{#if @report.data.length}}
|
||||||
<div class="rewind-report-page -most-viewed-tags">
|
<div class="rewind-report-page --most-viewed-tags">
|
||||||
<h2 class="rewind-report-title">{{i18n
|
<h2 class="rewind-report-title">{{i18n
|
||||||
"discourse_rewind.reports.most_viewed_tags.title"
|
"discourse_rewind.reports.most_viewed_tags.title"
|
||||||
[email protected]
|
[email protected]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class NewUserInteractions extends Component {
|
||||||
|
get wavyWords() {
|
||||||
|
const num = this.args.report.data.unique_new_users;
|
||||||
|
const memberText = i18n(
|
||||||
|
"discourse_rewind.reports.new_user_interactions.new_member",
|
||||||
|
{ count: num }
|
||||||
|
);
|
||||||
|
return memberText.split(" ").map((word) => word.split(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rewind-report-page --new-user-interactions">
|
||||||
|
<div class="wordart-container">
|
||||||
|
<div class="wordart-text">your contributions helped</div>
|
||||||
|
<div class="wordart-3d">
|
||||||
|
{{#each this.wavyWords as |word|}}
|
||||||
|
<span class="wordart-word">
|
||||||
|
{{#each word as |char|}}
|
||||||
|
<span class="wordart-letter">{{char}}</span>
|
||||||
|
{{/each}}
|
||||||
|
</span>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user