diff --git a/Gemfile b/Gemfile index c687986..39f7147 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,8 @@ gem 'ruby-openai' gem 'activegraph', '11.5.0.beta.3' gem 'neo4j-ruby-driver' gem 'yard' +gem 'csv' +gem 'ostruct' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.2.1" diff --git a/Gemfile.lock b/Gemfile.lock index 7c7ff74..8a64e4d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,6 +109,7 @@ GEM fiber-local json crass (1.0.6) + csv (3.3.0) date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -185,6 +186,7 @@ GEM nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) + ostruct (0.6.0) pg (1.5.7) pry (0.14.2) coderay (~> 1.1) @@ -334,10 +336,12 @@ DEPENDENCIES activegraph (= 11.5.0.beta.3) bootsnap capybara + csv debug dotenv faker (~> 3.4, >= 3.4.2) neo4j-ruby-driver + ostruct pg pry puma (>= 5.0) @@ -356,7 +360,7 @@ DEPENDENCIES yard RUBY VERSION - ruby 3.2.2p53 + ruby 3.3.5p100 BUNDLED WITH 2.5.6 diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index 039afb2..678de7d 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -8,3 +8,55 @@ --icon-zoom: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTIwMHB0IiBoZWlnaHQ9IjEyMDBwdCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPHBhdGggZD0ibTExMTUuOCA5NzIuOC0yMzUuMzgtMjM1LjMxYzQyLjgzNi02OC42OTEgNjcuOTY1LTE0OS40OSA2Ny45NjUtMjM2LjE0IDAtMjQ2LjI5LTIwMC41MS00NDYuOC00NDYuOTUtNDQ2LjhzLTQ0Ni44NyAyMDAuNTEtNDQ2Ljg3IDQ0Ni44YzAgMjQ2LjUxIDIwMC40NCA0NDcuMDkgNDQ2Ljg3IDQ0Ny4wOSA4Ni42NTYgMCAxNjcuNDItMjUuMTk5IDIzNi02Ny45NjVsMjM1LjQ1IDIzNS4zOGMxOS43NDYgMTkuNjAyIDQ1LjYwMiAyOS42MDIgNzEuNjAyIDI5LjYwMiAyNS43MTEgMCA1MS42MzctMTAgNzEuMzA5LTI5LjYwMiAzOS41MjctMzkuNDg4IDM5LjUyNy0xMDMuNDkgMC0xNDMuMDV6bS05MzQuOC00NzEuNDZjMC0xNzYuNTUgMTQzLjc1LTMyMC40IDMyMC40Ny0zMjAuNCAxNzYuODQgMCAzMjAuNTggMTQzLjgyIDMyMC41OCAzMjAuNCAwIDE3Ni43Ni0xNDMuNzUgMzIwLjY2LTMyMC41OCAzMjAuNjYtMTc2LjczIDAtMzIwLjQ3LTE0My44OS0zMjAuNDctMzIwLjY2eiIgZmlsbD0iIzAwMDAwMCIvPgo8L3N2Zz4=); --icon-zoom-light: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTIwMHB0IiBoZWlnaHQ9IjEyMDBwdCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPHBhdGggZD0ibTExMTUuOCA5NzIuOC0yMzUuMzgtMjM1LjMxYzQyLjgzNi02OC42OTEgNjcuOTY1LTE0OS40OSA2Ny45NjUtMjM2LjE0IDAtMjQ2LjI5LTIwMC41MS00NDYuOC00NDYuOTUtNDQ2LjhzLTQ0Ni44NyAyMDAuNTEtNDQ2Ljg3IDQ0Ni44YzAgMjQ2LjUxIDIwMC40NCA0NDcuMDkgNDQ2Ljg3IDQ0Ny4wOSA4Ni42NTYgMCAxNjcuNDItMjUuMTk5IDIzNi02Ny45NjVsMjM1LjQ1IDIzNS4zOGMxOS43NDYgMTkuNjAyIDQ1LjYwMiAyOS42MDIgNzEuNjAyIDI5LjYwMiAyNS43MTEgMCA1MS42MzctMTAgNzEuMzA5LTI5LjYwMiAzOS41MjctMzkuNDg4IDM5LjUyNy0xMDMuNDkgMC0xNDMuMDV6bS05MzQuOC00NzEuNDZjMC0xNzYuNTUgMTQzLjc1LTMyMC40IDMyMC40Ny0zMjAuNCAxNzYuODQgMCAzMjAuNTggMTQzLjgyIDMyMC41OCAzMjAuNCAwIDE3Ni43Ni0xNDMuNzUgMzIwLjY2LTMyMC41OCAzMjAuNjYtMTc2LjczIDAtMzIwLjQ3LTE0My44OS0zMjAuNDctMzIwLjY2eiIgZmlsbD0iI2ZmZiIvPgo8L3N2Zz4K); } + +.throbber, +.throbber div { + box-sizing: border-box; +} +.throbber { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.throbber div { + position: absolute; + border: 4px solid var(--color-green); + opacity: 1; + border-radius: 50%; + animation: throbber 2s cubic-bezier(0, 0.2, 0.8, 1) infinite; +} +.throbber div:nth-child(2) { + animation-delay: -0.5s; +} +@keyframes throbber { + 0% { + top: 36px; + left: 36px; + width: 8px; + height: 8px; + opacity: 0; + } + 4.9% { + top: 36px; + left: 36px; + width: 8px; + height: 8px; + opacity: 0; + } + 5% { + top: 36px; + left: 36px; + width: 8px; + height: 8px; + opacity: 1; + } + 100% { + top: 0; + left: 0; + width: 80px; + height: 80px; + opacity: 0; + } +} + diff --git a/app/assets/stylesheets/styles.css b/app/assets/stylesheets/styles.css index 7cc3f72..20732c6 100644 --- a/app/assets/stylesheets/styles.css +++ b/app/assets/stylesheets/styles.css @@ -76,7 +76,7 @@ ul, ol { list-style-position: inside; - margin: 1.5rem 0 1.5rem 1.5rem; + margin: 0 0 1.5rem 1.5rem; } em { diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index e571777..e255b91 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -4,6 +4,7 @@ class CategoriesController < ApplicationController def index @section_name = @question.label + @context = @question.context @categories = Category.where(context: @question.context.name).order(:name) @enqueued_at = params[:enqueued_at].present? ? Time.at(params[:enqueued_at].to_i).strftime("%T %Z") : nil end @@ -14,7 +15,8 @@ def show end def new - @category = Category.new(name: "New Category", context: @question.context.name) + @category = Category.new(name: "New Category", context: @question.context.name) + @context = @question.context end def create diff --git a/app/controllers/codebooks_controller.rb b/app/controllers/codebooks_controller.rb index 7890ec4..306e126 100644 --- a/app/controllers/codebooks_controller.rb +++ b/app/controllers/codebooks_controller.rb @@ -35,4 +35,16 @@ def show end + def enqueue_category_suggestions + question = Question.find(params[:codebook_id]) + context = question.context + CategorySuggestionsJob.perform_async(context.id) + respond_to do |format| + format.turbo_stream do + render turbo_stream: turbo_stream.replace("frame-suggestions", partial: "/categories/suggestions", locals: { context: context, question: question, enqueued: params[:enqueued] }) + end + end + + end + end diff --git a/app/jobs/category_suggestions_job.rb b/app/jobs/category_suggestions_job.rb new file mode 100644 index 0000000..9a91e3b --- /dev/null +++ b/app/jobs/category_suggestions_job.rb @@ -0,0 +1,15 @@ +# This background job performs the Category derivation process. +class CategorySuggestionsJob + + include Sidekiq::Job + + queue_as :default + + def perform(context_id) + Rails.logger.info("CategorySuggestionsJob running with context id #{context_id}") + return unless context = Context.find(context_id) + context.suggest_categories + context.update_attribute(:suggestions_updated_at, DateTime.now) + end + +end diff --git a/app/models/context.rb b/app/models/context.rb index f054b60..513c4a4 100644 --- a/app/models/context.rb +++ b/app/models/context.rb @@ -7,4 +7,10 @@ class Context < ApplicationRecord has_many :questions + def suggest_categories + update_attribute(:suggested_categories, []) + categories = Services::SuggestCategories.perform(self.id).map{|category| category['category'] } + update_attribute(:suggested_categories, categories) + end + end diff --git a/app/models/services/suggest_categories.rb b/app/models/services/suggest_categories.rb index b56e4af..0c91e32 100644 --- a/app/models/services/suggest_categories.rb +++ b/app/models/services/suggest_categories.rb @@ -1,7 +1,7 @@ module Services class SuggestCategories - attr_accessor :text + attr_accessor :context # This is the prompt sent to the selected AI agent to provide instructions on category derivision. PROMPT = %{ @@ -19,21 +19,25 @@ class SuggestCategories ] } - The list of codes is: + The array of codes is: } - def self.perform(text) - new(text).perform + def self.perform(context_id) + new(context_id).perform end - def initialize(text) - @text = text + def initialize(context_id) + @context = Context.find(context_id) end # Uses the OpenAI client to pass the prompt and text through the API for sentiment analysis. def perform - return false unless text.present? - response = Clients::OpenAi.request("#{PROMPT} #{text}") + return false unless context.present? + codes = Code.where(context: context.name).map(&:name) + + response = Clients::OpenAi.request("#{PROMPT} #{codes}") + # response = { "categories" => [ { "category" => "divisions" }, { "category" => "third space" }, { "category" => "intergenerational" } ] } + return false unless response['categories'].present? return response['categories'] end diff --git a/app/views/categories/_suggestions.html.erb b/app/views/categories/_suggestions.html.erb new file mode 100644 index 0000000..ed50ebd --- /dev/null +++ b/app/views/categories/_suggestions.html.erb @@ -0,0 +1,23 @@ +<%= turbo_frame_tag "frame-suggestions" do %> + + <% if enqueued && DateTime.parse(enqueued) > context.suggestions_updated_at %> +
Category suggestions were requested at <%= @enqueued_at %>. Please allow up to 2 minutes for the process to complete. You can reload the page to monitor progress.
-<% else %> - <%= button_to "Get Suggestions", codebook_enqueue_categories_path(@question.id, params: { enqueued: true }, method: :put ) %> -<% end %> +<%= render partial: "suggestions", locals: { context: @context, question: @question, enqueued: nil } %> <%= render partial: "/shared/filtering" %> diff --git a/app/views/categories/new.html.erb b/app/views/categories/new.html.erb index f2c8fb9..550568d 100644 --- a/app/views/categories/new.html.erb +++ b/app/views/categories/new.html.erb @@ -2,15 +2,32 @@