Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,18 @@ def user_not_authorized
flash[:alert] = 'You are not authorized to perform this action.'
redirect_back(fallback_location: root_path)
end

# Tracks a view for any viewable object (Document, Library, etc.)
# Updates the timestamp if the user has already viewed this item
# @param viewable [ActiveRecord::Base] the object being viewed (must have viewed_items association)
def track_view(viewable)
return unless current_user

viewed_item = ViewedItem.find_or_initialize_by(
user: current_user,
viewable: viewable
)
viewed_item.viewed_at = Time.current
viewed_item.save
end
end
4 changes: 4 additions & 0 deletions app/controllers/dashboard_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ def index
.group('assistants.id')
.order('last_chat_time DESC')
.limit(5)
@recently_viewed_documents = current_user.recently_viewed_documents(limit: 5)
.includes(:viewed_items)
@recently_viewed_libraries = current_user.recently_viewed_libraries(limit: 5)
.includes(:viewed_items)
end
end
3 changes: 3 additions & 0 deletions app/controllers/documents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ def edit

# GET /documents/1 or /documents/1.json
def show
# Track view for authenticated users
track_view(@document)

# Handle flagging functionality (requires user to be logged in)
if params[:flag] && current_user
@document.disliked_by current_user, vote_scope: 'flag'
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/libraries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ def edit
end

# GET /libraries/1 or /libraries/1.json
def show; end
def show
# Track view for authenticated users
track_view(@library)
end

def users
@library = Library.find(params[:id])
Expand Down
15 changes: 15 additions & 0 deletions app/models/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ class Document < ApplicationRecord
has_many :comments, dependent: :destroy
has_neighbors :embedding

# Recently viewed items feature
has_many :viewed_items, as: :viewable, dependent: :destroy

# Returns the number of unique users who have viewed this document
# Note: Due to the unique index on [user_id, viewable_type, viewable_id],
# each user can only have one view record per document. When a user views
# the document multiple times, only the timestamp is updated.
# @return [Integer] the number of unique users who have viewed this document
def unique_viewers
viewed_items.count
end

# Alias for backward compatibility
alias total_views unique_viewers

# Primary search scope using PostgreSQL full-text search
# Searches across both title and document content with strict word matching
# - prefix: true allows partial word matching (e.g., "test" matches "testing")
Expand Down
3 changes: 3 additions & 0 deletions app/models/library.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class Library < ApplicationRecord
has_many :library_users, dependent: :destroy
has_many :users, through: :library_users

# Recently viewed items feature
has_many :viewed_items, as: :viewable, dependent: :destroy

pg_search_scope :search_by_name,
against: %i[name description],
using: {
Expand Down
38 changes: 38 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,44 @@ class User < ApplicationRecord
has_many :assistants, through: :assistant_users
has_many :comments, dependent: :destroy

# Recently viewed items feature
has_many :viewed_items, dependent: :destroy

# Returns the most recently viewed documents for this user
# @param limit [Integer] the maximum number of documents to return (default: 5)
# @return [ActiveRecord::Relation<Document>] the recently viewed documents, ordered by most recent first
def recently_viewed_documents(limit: 5)
Document.joins(:viewed_items)
.where(viewed_items: { user_id: id })
.order('viewed_items.viewed_at DESC')
.distinct
.limit(limit)
end

# Returns the most recently viewed libraries for this user
# @param limit [Integer] the maximum number of libraries to return (default: 5)
# @return [ActiveRecord::Relation<Library>] the recently viewed libraries, ordered by most recent first
def recently_viewed_libraries(limit: 5)
Library.joins(:viewed_items)
.where(viewed_items: { user_id: id })
.order('viewed_items.viewed_at DESC')
.distinct
.limit(limit)
end

# Generic method to get recently viewed items of any type
# @param viewable_type [String] the type of viewable items to retrieve (e.g., 'Document')
# @param limit [Integer] the maximum number of items to return (default: 5)
# @return [ActiveRecord::Relation] the recently viewed items
def recently_viewed(viewable_type:, limit: 5)
viewable_type.constantize
.joins(:viewed_items)
.where(viewed_items: { user_id: id })
.order('viewed_items.viewed_at DESC')
.distinct
.limit(limit)
end

private

def password_strength
Expand Down
20 changes: 20 additions & 0 deletions app/models/viewed_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class ViewedItem < ApplicationRecord
belongs_to :user
belongs_to :viewable, polymorphic: true

validates :user_id, presence: true
validates :viewable_id, presence: true
validates :viewable_type, presence: true
validates :viewed_at, presence: true

# Ensure uniqueness at the model level as well
validates :user_id, uniqueness: { scope: %i[viewable_type viewable_id] }

# Scope to get most recently viewed items
scope :recent, -> { order(viewed_at: :desc) }

# Scope to get items for a specific viewable type
scope :for_type, ->(type) { where(viewable_type: type) }
end
61 changes: 47 additions & 14 deletions app/views/dashboard/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
My Activity
</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<!-- Questions -->
<div class="bg-white rounded-lg p-6 border border-gray-300">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-700 flex items-center">
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
Expand Down Expand Up @@ -44,7 +44,7 @@
<!-- Chats -->
<div class="bg-white rounded-lg p-6 border border-gray-300">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-700 flex items-center">
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
</svg>
Expand All @@ -70,32 +70,65 @@
<% end %>
</div>
</div>
<!-- Assistants -->

<!-- Recently Viewed Documents -->
<div class="bg-white rounded-lg p-6 border border-gray-300">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-700 flex items-center">
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
Assistants
Documents
</h2>
<%= link_to assistants_path, class: "inline-flex items-center px-3 py-1 border border-sky-500 text-sky-500 text-sm font-medium rounded-md hover:bg-sky-500 hover:text-white transition-colors duration-200" do %>
<%= link_to documents_path, class: "inline-flex items-center px-3 py-1 border border-sky-500 text-sky-500 text-sm font-medium rounded-md hover:bg-sky-500 hover:text-white transition-colors duration-200" do %>
More
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<% end %>
</div>
<div class="">
<% if @recent_assistants.any? %>
<% @recent_assistants.each do |assistant| %>
<% if @recently_viewed_documents.any? %>
<% @recently_viewed_documents.each do |document| %>
<% viewed_item = document.viewed_items.find_by(user: current_user) %>
<%= render "dashboard/list_item",
path: assistant_path(assistant),
title: assistant.name,
subtitle: "Last active #{time_ago_in_words(assistant.last_chat_time)} ago" %>
path: document_path(document),
title: document.title.truncate(50),
subtitle: "Viewed #{time_ago_in_words(viewed_item.viewed_at)} ago" %>
<% end %>
<% else %>
<p class="text-gray-400 italic">No recent assistants</p>
<p class="text-gray-400 italic">No recently viewed documents</p>
<% end %>
</div>
</div>
<!-- Recently Viewed Libraries -->
<div class="bg-white rounded-lg p-6 border border-gray-300">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
</svg>
Libraries
</h2>
<%= link_to libraries_path, class: "inline-flex items-center px-3 py-1 border border-sky-500 text-sky-500 text-sm font-medium rounded-md hover:bg-sky-500 hover:text-white transition-colors duration-200" do %>
More
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<% end %>
</div>
<div class="">
<% if @recently_viewed_libraries.any? %>
<% @recently_viewed_libraries.each do |library| %>
<% viewed_item = library.viewed_items.find_by(user: current_user) %>
<%= render "dashboard/list_item",
path: library_path(library),
title: library.name.truncate(50),
subtitle: "Viewed #{time_ago_in_words(viewed_item.viewed_at)} ago" %>
<% end %>
<% else %>
<p class="text-gray-400 italic">No recently viewed libraries</p>
<% end %>
</div>
</div>
Expand Down
11 changes: 10 additions & 1 deletion app/views/documents/_document.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"turbo-method": "delete",
"turbo-confirm": "Are you sure you want to delete this document?"
},
class: "border text-red-700 border-red-300 py-3 px-5 bg-white text-sm #{'rounded-l-lg' if document.deleted?} rounded-r-lg hover:bg-red-50 hover:text-red-800" %>
class: "border border-sky-500 text-red-700 py-3 px-5 bg-white text-sm #{'rounded-l-lg' if document.deleted?} rounded-r-lg hover:bg-red-50 hover:text-red-800" %>
<% end %>
<% end %>
</div>
Expand Down Expand Up @@ -62,6 +62,15 @@

<!-- Voting and Flag Buttons on the right side -->
<div class="flex items-center mx-4 space-x-2">
<!-- Unique Viewers Count -->
<div class="flex items-center px-3 py-1 text-sm bg-blue-50 text-blue-800 border border-blue-200 rounded-full" title="Unique viewers">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
<%= document.unique_viewers %>
</div>

<!-- Upvote/Downvote Buttons -->
<% if current_user %>
<% upvote_count = @document.votes_for.where(vote_scope: 'rating', vote_flag: true).count %>
Expand Down
20 changes: 20 additions & 0 deletions db/migrate/20251017181945_create_viewed_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class CreateViewedItems < ActiveRecord::Migration[7.2]
def change
create_table :viewed_items do |t|
t.references :user, null: false, foreign_key: true
t.references :viewable, polymorphic: true, null: false
t.datetime :viewed_at

t.timestamps
end

# Add composite index for efficient queries by user
add_index :viewed_items, %i[user_id viewed_at]

# Add composite index for efficient queries by viewable
add_index :viewed_items, %i[viewable_type viewable_id viewed_at]

# Add unique index to ensure one view record per user-viewable combination
add_index :viewed_items, %i[user_id viewable_type viewable_id], unique: true, name: 'index_viewed_items_on_user_and_viewable'
end
end
17 changes: 16 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading