From 3f01bcb32ed4453a5bcee4f81cf3f59fe50867c0 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 14 Feb 2024 15:25:34 -0800 Subject: [PATCH 1/7] DEV: Make prompts available on `CurrentUserSerializer` --- .../after-d-editor/ai-helper-context-menu.js | 20 +++------------- .../ai-edit-suggestion-button.gjs | 23 +++++-------------- .../ai-helper-options-menu.gjs | 15 +++--------- lib/ai_helper/entry_point.rb | 14 +++++++++++ 4 files changed, 26 insertions(+), 46 deletions(-) diff --git a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js index 2b4a3fd91..07a32f030 100644 --- a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js +++ b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js @@ -19,7 +19,6 @@ export default class AiHelperContextMenu extends Component { @service siteSettings; @service modal; @service capabilities; - @tracked helperOptions = []; @tracked showContextMenu = false; @tracked caretCoords; @tracked virtualElement; @@ -56,15 +55,6 @@ export default class AiHelperContextMenu extends Component { @tracked _contextMenu; @tracked _activeAIRequest = null; - constructor() { - super(...arguments); - - // Fetch prompts only if it hasn't been fetched yet - if (this.helperOptions.length === 0) { - this.loadPrompts(); - } - } - willDestroy() { super.willDestroy(...arguments); document.removeEventListener("selectionchange", this.selectionChanged); @@ -81,8 +71,8 @@ export default class AiHelperContextMenu extends Component { this._menuState = newState; } - async loadPrompts() { - let prompts = await ajax("/discourse-ai/ai-helper/prompts"); + get helperOptions() { + let prompts = this.currentUser?.ai_helper_prompts; prompts = prompts .filter((p) => p.location.includes("composer")) @@ -109,7 +99,7 @@ export default class AiHelperContextMenu extends Component { memo[p.name] = p.prompt_type; return memo; }, {}); - this.helperOptions = prompts; + return prompts; } @bind @@ -338,10 +328,6 @@ export default class AiHelperContextMenu extends Component { @action toggleAiHelperOptions() { - // Fetch prompts only if it hasn't been fetched yet - if (this.helperOptions.length === 0) { - this.loadPrompts(); - } this.menuState = this.CONTEXT_MENU_STATES.options; } diff --git a/assets/javascripts/discourse/connectors/fast-edit-footer-after/ai-edit-suggestion-button.gjs b/assets/javascripts/discourse/connectors/fast-edit-footer-after/ai-edit-suggestion-button.gjs index 1531d3103..c09bd43f8 100644 --- a/assets/javascripts/discourse/connectors/fast-edit-footer-after/ai-edit-suggestion-button.gjs +++ b/assets/javascripts/discourse/connectors/fast-edit-footer-after/ai-edit-suggestion-button.gjs @@ -1,6 +1,7 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; import DButton from "discourse/components/d-button"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -11,31 +12,19 @@ export default class AiEditSuggestionButton extends Component { return showPostAIHelper(outletArgs, helper); } + @service currentUser; @tracked loading = false; @tracked suggestion = ""; @tracked _activeAIRequest = null; - constructor() { - super(...arguments); - - if (!this.mode) { - this.loadMode(); - } - } - get disabled() { return this.loading || this.suggestion?.length > 0; } - async loadMode() { - let mode = await ajax("/discourse-ai/ai-helper/prompts", { - method: "GET", - data: { - name_filter: "proofread", - }, - }); - - this.mode = mode[0]; + get mode() { + return this.currentUser?.ai_helper_prompts.find( + (prompt) => prompt.name === "proofread" + ); } @action diff --git a/assets/javascripts/discourse/connectors/post-text-buttons/ai-helper-options-menu.gjs b/assets/javascripts/discourse/connectors/post-text-buttons/ai-helper-options-menu.gjs index 4e35bb2ff..d00fa7805 100644 --- a/assets/javascripts/discourse/connectors/post-text-buttons/ai-helper-options-menu.gjs +++ b/assets/javascripts/discourse/connectors/post-text-buttons/ai-helper-options-menu.gjs @@ -31,7 +31,6 @@ export default class AIHelperOptionsMenu extends Component { @service currentUser; @service menu; - @tracked helperOptions = []; @tracked menuState = this.MENU_STATES.triggers; @tracked loading = false; @tracked suggestion = ""; @@ -51,14 +50,6 @@ export default class AIHelperOptionsMenu extends Component { @tracked _activeAIRequest = null; - constructor() { - super(...arguments); - - if (this.helperOptions.length === 0) { - this.loadPrompts(); - } - } - @action async showAIHelperOptions() { this.showMainButtons = false; @@ -168,8 +159,8 @@ export default class AIHelperOptionsMenu extends Component { } } - async loadPrompts() { - let prompts = await ajax("/discourse-ai/ai-helper/prompts"); + get helperOptions() { + let prompts = this.currentUser?.ai_helper_prompts; prompts = prompts.filter((item) => item.location.includes("post")); @@ -191,7 +182,7 @@ export default class AIHelperOptionsMenu extends Component { prompts = prompts.filter((p) => p.name !== "proofread"); } - this.helperOptions = prompts; + return prompts; } _showUserCustomPrompts() { diff --git a/lib/ai_helper/entry_point.rb b/lib/ai_helper/entry_point.rb index 87ebca244..840d2b55a 100644 --- a/lib/ai_helper/entry_point.rb +++ b/lib/ai_helper/entry_point.rb @@ -19,6 +19,20 @@ def inject_into(plugin) thread_id: thread.id, ) end + + plugin.add_to_serializer( + :current_user, + :ai_helper_prompts, + include_condition: -> do + SiteSetting.composer_ai_helper_enabled && scope.authenticated? && + scope.user.in_any_groups?(SiteSetting.ai_helper_allowed_groups_map) + end, + ) do + ActiveModel::ArraySerializer.new( + DiscourseAi::AiHelper::Assistant.new.available_prompts, + root: false, + ) + end end end end From 762184594a6046465283e3aef22eab284f3d31fd Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 14 Feb 2024 15:29:48 -0800 Subject: [PATCH 2/7] DEV: No longer need `prompts` endpoint --- .../ai_helper/assistant_controller.rb | 11 ----- config/routes.rb | 1 - .../ai_helper/assistant_controller_spec.rb | 47 ------------------- 3 files changed, 59 deletions(-) diff --git a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb index c3e08f4cd..76a3b3b2c 100644 --- a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb +++ b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb @@ -8,17 +8,6 @@ class AssistantController < ::ApplicationController before_action :ensure_can_request_suggestions before_action :rate_limiter_performed!, except: %i[prompts] - def prompts - name_filter = params[:name_filter] - - render json: - ActiveModel::ArraySerializer.new( - DiscourseAi::AiHelper::Assistant.new.available_prompts(name_filter: name_filter), - root: false, - ), - status: 200 - end - def suggest input = get_text_param! diff --git a/config/routes.rb b/config/routes.rb index 66a29e020..d6e1fe94e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ DiscourseAi::Engine.routes.draw do scope module: :ai_helper, path: "/ai-helper", defaults: { format: :json } do - get "prompts" => "assistant#prompts" post "suggest" => "assistant#suggest" post "suggest_title" => "assistant#suggest_title" post "suggest_category" => "assistant#suggest_category" diff --git a/spec/requests/ai_helper/assistant_controller_spec.rb b/spec/requests/ai_helper/assistant_controller_spec.rb index 5902ba1bf..8b262856d 100644 --- a/spec/requests/ai_helper/assistant_controller_spec.rb +++ b/spec/requests/ai_helper/assistant_controller_spec.rb @@ -107,51 +107,4 @@ end end end - - describe "#prompts" do - context "when not logged in" do - it "returns a 403 response" do - get "/discourse-ai/ai-helper/prompts" - expect(response.status).to eq(403) - end - end - - context "when logged in as a user without enough privileges" do - fab!(:user) { Fabricate(:newuser) } - - before do - sign_in(user) - SiteSetting.ai_helper_allowed_groups = Group::AUTO_GROUPS[:staff] - end - - it "returns a 403 response" do - get "/discourse-ai/ai-helper/prompts" - expect(response.status).to eq(403) - end - end - - context "when logged in as an allowed user" do - fab!(:user) { Fabricate(:user) } - - before do - sign_in(user) - user.group_ids = [Group::AUTO_GROUPS[:trust_level_1]] - SiteSetting.ai_helper_allowed_groups = Group::AUTO_GROUPS[:trust_level_1] - SiteSetting.ai_helper_illustrate_post_model = "stable_diffusion_xl" - end - - it "returns a list of prompts when no name_filter is provided" do - get "/discourse-ai/ai-helper/prompts" - expect(response.status).to eq(200) - expect(response.parsed_body.length).to eq(7) - end - - it "returns a list with with filtered prompts when name_filter is provided" do - get "/discourse-ai/ai-helper/prompts", params: { name_filter: "proofread" } - expect(response.status).to eq(200) - expect(response.parsed_body.length).to eq(1) - expect(response.parsed_body.first["name"]).to eq("proofread") - end - end - end end From 2459e763f19574cedc59d93597fc1bd88c42f142 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 15 Feb 2024 11:25:58 -0800 Subject: [PATCH 3/7] DEV: Add spec --- spec/plugin_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/plugin_spec.rb b/spec/plugin_spec.rb index e50230a87..e57dddd10 100644 --- a/spec/plugin_spec.rb +++ b/spec/plugin_spec.rb @@ -23,4 +23,21 @@ expect(accuracy.flags_agreed).to eq(1) end end + + describe "current_user_serializer#ai_helper_prompts" do + fab!(:user) + + before do + SiteSetting.ai_helper_model = "fake:fake" + SiteSetting.composer_ai_helper_enabled = true + Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user) + end + + let(:serializer) { CurrentUserSerializer.new(user, scope: Guardian.new(user)) } + + it "returns the available prompts" do + expect(serializer.ai_helper_prompts).to be_present + expect(serializer.ai_helper_prompts.object.count).to eq(6) + end + end end From 5e5d682f2dfceafec5c5629a0d6872151df17ed9 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 15 Feb 2024 12:01:13 -0800 Subject: [PATCH 4/7] DEV: Cache prompts --- lib/ai_helper/assistant.rb | 47 +++++++++++--------- spec/lib/modules/ai_helper/assistant_spec.rb | 9 ---- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb index 0c4078b55..62c845847 100644 --- a/lib/ai_helper/assistant.rb +++ b/lib/ai_helper/assistant.rb @@ -3,35 +3,40 @@ module DiscourseAi module AiHelper class Assistant - def available_prompts(name_filter: nil) - cp = CompletionPrompt - prompts = [] + AI_HELPER_PROMPTS_CACHE_KEY = "ai_helper_prompts" + + def available_prompts + prompts = Discourse.cache.read(AI_HELPER_PROMPTS_CACHE_KEY) + + if !prompts + prompts = CompletionPrompt.where(enabled: true) - if name_filter - prompts = [cp.enabled_by_name(name_filter)] - else - prompts = cp.where(enabled: true) # Hide illustrate_post if disabled prompts = prompts.where.not( name: "illustrate_post", ) if SiteSetting.ai_helper_illustrate_post_model == "disabled" - end - prompts.map do |prompt| - translation = - I18n.t("discourse_ai.ai_helper.prompts.#{prompt.name}", default: nil) || - prompt.translated_name || prompt.name - - { - id: prompt.id, - name: prompt.name, - translated_name: translation, - prompt_type: prompt.prompt_type, - icon: icon_map(prompt.name), - location: location_map(prompt.name), - } + prompts = + prompts.map do |prompt| + translation = + I18n.t("discourse_ai.ai_helper.prompts.#{prompt.name}", default: nil) || + prompt.translated_name || prompt.name + + { + id: prompt.id, + name: prompt.name, + translated_name: translation, + prompt_type: prompt.prompt_type, + icon: icon_map(prompt.name), + location: location_map(prompt.name), + } + end + + Discourse.cache.write(AI_HELPER_PROMPTS_CACHE_KEY, prompts) end + + prompts end def generate_prompt(completion_prompt, input, user, &block) diff --git a/spec/lib/modules/ai_helper/assistant_spec.rb b/spec/lib/modules/ai_helper/assistant_spec.rb index 875ae6a97..74e4669ec 100644 --- a/spec/lib/modules/ai_helper/assistant_spec.rb +++ b/spec/lib/modules/ai_helper/assistant_spec.rb @@ -29,15 +29,6 @@ end end - context "when name filter is provided" do - it "returns the prompt with the given name" do - prompts = subject.available_prompts(name_filter: "translate") - - expect(prompts.length).to eq(1) - expect(prompts.first[:name]).to eq("translate") - end - end - context "when illustrate post model is enabled" do before { SiteSetting.ai_helper_illustrate_post_model = "stable_diffusion_xl" } From dd7b4402764ea65c527c5c5bb97aabcd4bb59f33 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 15 Feb 2024 12:09:21 -0800 Subject: [PATCH 5/7] DEV: Explicitly set plugin setting for consistent env --- spec/plugin_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/plugin_spec.rb b/spec/plugin_spec.rb index e57dddd10..4cb0240db 100644 --- a/spec/plugin_spec.rb +++ b/spec/plugin_spec.rb @@ -30,6 +30,7 @@ before do SiteSetting.ai_helper_model = "fake:fake" SiteSetting.composer_ai_helper_enabled = true + SiteSetting.ai_helper_illustrate_post_model = "disabled" Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user) end From 0fc086bf1fa5cba8f97e88a74a43af7a3f446775 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 15 Feb 2024 12:16:54 -0800 Subject: [PATCH 6/7] DEV: Clear cache before checking spec --- spec/lib/modules/ai_helper/assistant_spec.rb | 34 ++++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/spec/lib/modules/ai_helper/assistant_spec.rb b/spec/lib/modules/ai_helper/assistant_spec.rb index 74e4669ec..c7e59c96d 100644 --- a/spec/lib/modules/ai_helper/assistant_spec.rb +++ b/spec/lib/modules/ai_helper/assistant_spec.rb @@ -13,24 +13,30 @@ STRING describe("#available_prompts") do - context "when no name filter is provided" do - it "returns all available prompts" do - prompts = subject.available_prompts + before do + SiteSetting.ai_helper_illustrate_post_model = "disabled" + Discourse.cache.delete(DiscourseAi::AiHelper::Assistant::AI_HELPER_PROMPTS_CACHE_KEY) + end - expect(prompts.length).to eq(6) - expect(prompts.map { |p| p[:name] }).to contain_exactly( - "translate", - "generate_titles", - "proofread", - "markdown_table", - "custom_prompt", - "explain", - ) - end + it "returns all available prompts" do + prompts = subject.available_prompts + + expect(prompts.length).to eq(6) + expect(prompts.map { |p| p[:name] }).to contain_exactly( + "translate", + "generate_titles", + "proofread", + "markdown_table", + "custom_prompt", + "explain", + ) end context "when illustrate post model is enabled" do - before { SiteSetting.ai_helper_illustrate_post_model = "stable_diffusion_xl" } + before do + SiteSetting.ai_helper_illustrate_post_model = "stable_diffusion_xl" + Discourse.cache.delete(DiscourseAi::AiHelper::Assistant::AI_HELPER_PROMPTS_CACHE_KEY) + end it "returns the illustrate_post prompt in the list of all prompts" do prompts = subject.available_prompts From d8e12ac029171a37076e674e46f6a6f04c83987f Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 15 Feb 2024 13:08:55 -0800 Subject: [PATCH 7/7] DEV: Use fetch instead of read/write --- lib/ai_helper/assistant.rb | 59 ++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb index 62c845847..4fa8efe41 100644 --- a/lib/ai_helper/assistant.rb +++ b/lib/ai_helper/assistant.rb @@ -6,37 +6,34 @@ class Assistant AI_HELPER_PROMPTS_CACHE_KEY = "ai_helper_prompts" def available_prompts - prompts = Discourse.cache.read(AI_HELPER_PROMPTS_CACHE_KEY) - - if !prompts - prompts = CompletionPrompt.where(enabled: true) - - # Hide illustrate_post if disabled - prompts = - prompts.where.not( - name: "illustrate_post", - ) if SiteSetting.ai_helper_illustrate_post_model == "disabled" - - prompts = - prompts.map do |prompt| - translation = - I18n.t("discourse_ai.ai_helper.prompts.#{prompt.name}", default: nil) || - prompt.translated_name || prompt.name - - { - id: prompt.id, - name: prompt.name, - translated_name: translation, - prompt_type: prompt.prompt_type, - icon: icon_map(prompt.name), - location: location_map(prompt.name), - } - end - - Discourse.cache.write(AI_HELPER_PROMPTS_CACHE_KEY, prompts) - end - - prompts + Discourse + .cache + .fetch(AI_HELPER_PROMPTS_CACHE_KEY) do + prompts = CompletionPrompt.where(enabled: true) + + # Hide illustrate_post if disabled + prompts = + prompts.where.not( + name: "illustrate_post", + ) if SiteSetting.ai_helper_illustrate_post_model == "disabled" + + prompts = + prompts.map do |prompt| + translation = + I18n.t("discourse_ai.ai_helper.prompts.#{prompt.name}", default: nil) || + prompt.translated_name || prompt.name + + { + id: prompt.id, + name: prompt.name, + translated_name: translation, + prompt_type: prompt.prompt_type, + icon: icon_map(prompt.name), + location: location_map(prompt.name), + } + end + prompts + end end def generate_prompt(completion_prompt, input, user, &block)