mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-05-15 12:59:41 +00:00
This second post was not used and is making the topic scroll down to post 2, which can mess with editing the OP sometimes
408 lines
14 KiB
Ruby
408 lines
14 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
RSpec.describe "AI Composer helper", type: :system, js: true do
|
||
fab!(:user) { Fabricate(:admin, refresh_auto_groups: true) }
|
||
fab!(:non_member_group) { Fabricate(:group) }
|
||
fab!(:embedding_definition)
|
||
|
||
before do
|
||
Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user)
|
||
assign_fake_provider_to(:ai_helper_model)
|
||
SiteSetting.ai_helper_enabled = true
|
||
sign_in(user)
|
||
end
|
||
|
||
let(:input) { "The rain in spain stays mainly in the Plane." }
|
||
let(:composer) { PageObjects::Components::Composer.new }
|
||
let(:ai_helper_menu) { PageObjects::Components::AiComposerHelperMenu.new }
|
||
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
|
||
let(:ai_suggestion_dropdown) { PageObjects::Components::AiSuggestionDropdown.new }
|
||
let(:toasts) { PageObjects::Components::Toasts.new }
|
||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||
|
||
fab!(:category)
|
||
fab!(:category_2) { Fabricate(:category) }
|
||
fab!(:video) { Fabricate(:tag) }
|
||
fab!(:music) { Fabricate(:tag) }
|
||
fab!(:cloud) { Fabricate(:tag) }
|
||
fab!(:feedback) { Fabricate(:tag) }
|
||
fab!(:review) { Fabricate(:tag) }
|
||
fab!(:topic) { Fabricate(:topic, category: category, tags: [video, music]) }
|
||
fab!(:post) do
|
||
Fabricate(
|
||
:post,
|
||
topic: topic,
|
||
raw:
|
||
"I like to eat pie. It is a very good dessert. Some people are wasteful by throwing pie at others but I do not do that. I always eat the pie.",
|
||
)
|
||
end
|
||
|
||
def trigger_composer_helper(content)
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(content)
|
||
composer.click_toolbar_button("ai-helper-trigger")
|
||
end
|
||
|
||
context "when triggering composer AI helper" do
|
||
it "shows the context menu when clicking the AI button in the composer toolbar" do
|
||
trigger_composer_helper(input)
|
||
expect(ai_helper_menu).to have_context_menu
|
||
end
|
||
|
||
it "shows a toast error when clicking the AI button without content" do
|
||
trigger_composer_helper("")
|
||
expect(ai_helper_menu).to have_no_context_menu
|
||
expect(toasts).to have_error(I18n.t("js.discourse_ai.ai_helper.no_content_error"))
|
||
end
|
||
|
||
it "shows prompt options in menu when AI button is clicked" do
|
||
trigger_composer_helper(input)
|
||
expect(ai_helper_menu).to be_showing_options
|
||
end
|
||
|
||
context "when using custom prompt" do
|
||
let(:mode) { CompletionPrompt::CUSTOM_PROMPT }
|
||
|
||
let(:custom_prompt_input) { "Translate to French" }
|
||
let(:custom_prompt_response) { "La pluie en Espagne reste principalement dans l'avion." }
|
||
|
||
it "shows custom prompt option" do
|
||
trigger_composer_helper(input)
|
||
expect(ai_helper_menu).to have_custom_prompt
|
||
end
|
||
|
||
it "enables the custom prompt button when input is filled" do
|
||
trigger_composer_helper(input)
|
||
expect(ai_helper_menu).to have_custom_prompt_button_disabled
|
||
ai_helper_menu.fill_custom_prompt(custom_prompt_input)
|
||
expect(ai_helper_menu).to have_custom_prompt_button_enabled
|
||
end
|
||
|
||
it "replaces the composed message with AI generated content" do
|
||
skip("Message bus updates not appearing in tests")
|
||
trigger_composer_helper(input)
|
||
ai_helper_menu.fill_custom_prompt(custom_prompt_input)
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([custom_prompt_response]) do
|
||
ai_helper_menu.click_custom_prompt_button
|
||
diff_modal.confirm_changes
|
||
wait_for { composer.composer_input.value == custom_prompt_response }
|
||
expect(composer.composer_input.value).to eq(custom_prompt_response)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when not a member of custom prompt group" do
|
||
let(:mode) { CompletionPrompt::CUSTOM_PROMPT }
|
||
before { SiteSetting.ai_helper_custom_prompts_allowed_groups = non_member_group.id.to_s }
|
||
|
||
it "does not show custom prompt option" do
|
||
trigger_composer_helper(input)
|
||
expect(ai_helper_menu).to have_no_custom_prompt
|
||
end
|
||
end
|
||
|
||
context "when using translation mode" do
|
||
let(:mode) { CompletionPrompt::TRANSLATE }
|
||
|
||
let(:spanish_input) { "La lluvia en España se queda principalmente en el avión." }
|
||
|
||
it "replaces the composed message with AI generated content" do
|
||
skip("Message bus updates not appearing in tests")
|
||
trigger_composer_helper(spanish_input)
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
||
ai_helper_menu.select_helper_model(mode)
|
||
diff_modal.confirm_changes
|
||
wait_for { composer.composer_input.value == input }
|
||
expect(composer.composer_input.value).to eq(input)
|
||
end
|
||
end
|
||
|
||
it "reverts results when Ctrl/Cmd + Z is pressed on the keyboard" do
|
||
skip("Message bus updates not appearing in tests")
|
||
trigger_composer_helper(spanish_input)
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
||
ai_helper_menu.select_helper_model(mode)
|
||
diff_modal.confirm_changes
|
||
wait_for { composer.composer_input.value == input }
|
||
ai_helper_menu.press_undo_keys
|
||
expect(composer.composer_input.value).to eq(spanish_input)
|
||
end
|
||
end
|
||
|
||
it "shows the changes in a modal" do
|
||
skip("Message bus updates not appearing in tests")
|
||
trigger_composer_helper(spanish_input)
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
||
ai_helper_menu.select_helper_model(mode)
|
||
|
||
expect(diff_modal).to be_visible
|
||
expect(diff_modal.old_value).to eq(spanish_input.gsub(/[[:space:]]+/, " ").strip)
|
||
expect(diff_modal.new_value).to eq(
|
||
input.gsub(/[[:space:]]+/, " ").gsub(/[‘’]/, "'").gsub(/[“”]/, '"').strip,
|
||
)
|
||
diff_modal.confirm_changes
|
||
expect(ai_helper_menu).to have_no_context_menu
|
||
end
|
||
end
|
||
|
||
it "does not apply the changes when discard button is pressed in the modal" do
|
||
trigger_composer_helper(spanish_input)
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
||
ai_helper_menu.select_helper_model(mode)
|
||
expect(diff_modal).to be_visible
|
||
diff_modal.discard_changes
|
||
expect(ai_helper_menu).to have_no_context_menu
|
||
expect(composer.composer_input.value).to eq(spanish_input)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when using the proofreading mode" do
|
||
let(:mode) { CompletionPrompt::PROOFREAD }
|
||
|
||
let(:proofread_text) { "The rain in Spain, stays mainly in the Plane." }
|
||
|
||
it "replaces the composed message with AI generated content" do
|
||
skip("Message bus updates not appearing in tests")
|
||
trigger_composer_helper(input)
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([proofread_text]) do
|
||
ai_helper_menu.select_helper_model(mode)
|
||
diff_modal.confirm_changes
|
||
wait_for { composer.composer_input.value == proofread_text }
|
||
expect(composer.composer_input.value).to eq(proofread_text)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when suggesting titles with AI title suggester" do
|
||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
||
|
||
let(:titles) do
|
||
"<item>Rainy Spain</item><item>Plane-Bound Delights</item><item>Mysterious Spain</item><item>Plane-Rain Chronicles</item><item>Unveiling Spain</item>"
|
||
end
|
||
|
||
it "opens a menu with title suggestions" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([titles]) do
|
||
ai_suggestion_dropdown.click_suggest_titles_button
|
||
|
||
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
||
|
||
expect(ai_suggestion_dropdown).to have_dropdown
|
||
end
|
||
end
|
||
|
||
it "replaces the topic title with the selected title" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([titles]) do
|
||
ai_suggestion_dropdown.click_suggest_titles_button
|
||
|
||
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
||
|
||
ai_suggestion_dropdown.select_suggestion_by_value(1)
|
||
expected_title = "Plane-Bound Delights"
|
||
|
||
expect(find("#reply-title").value).to eq(expected_title)
|
||
end
|
||
end
|
||
|
||
it "closes the menu when clicking outside" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([titles]) do
|
||
ai_suggestion_dropdown.click_suggest_titles_button
|
||
|
||
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
||
|
||
find(".d-editor-preview").click
|
||
|
||
expect(ai_suggestion_dropdown).to have_no_dropdown
|
||
end
|
||
end
|
||
|
||
it "only shows trigger button if there is sufficient content in the composer" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content("abc")
|
||
|
||
expect(ai_suggestion_dropdown).to have_no_suggestion_button
|
||
|
||
composer.fill_content(input)
|
||
expect(ai_suggestion_dropdown).to have_suggestion_button
|
||
end
|
||
end
|
||
|
||
context "when suggesting the category with AI category suggester" do
|
||
before do
|
||
SiteSetting.ai_embeddings_selected_model = embedding_definition.id
|
||
SiteSetting.ai_embeddings_enabled = true
|
||
end
|
||
|
||
it "updates the category with the suggested category" do
|
||
response =
|
||
Category
|
||
.take(3)
|
||
.map do |category|
|
||
{
|
||
id: category.id,
|
||
name: category.name,
|
||
slug: category.slug,
|
||
color: category.color,
|
||
score: rand(0.0...45.0),
|
||
topicCount: rand(1..3),
|
||
}
|
||
end
|
||
.sort_by { |h| h[:score] }
|
||
DiscourseAi::AiHelper::SemanticCategorizer.any_instance.stubs(:categories).returns(response)
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
ai_suggestion_dropdown.click_suggest_category_button
|
||
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
||
suggestion = category_2.name
|
||
ai_suggestion_dropdown.select_suggestion_by_name(suggestion)
|
||
category_selector = page.find(".category-chooser summary")
|
||
expect(category_selector["data-name"]).to eq(suggestion)
|
||
end
|
||
end
|
||
|
||
context "when suggesting the tags with AI tag suggester" do
|
||
before do
|
||
SiteSetting.ai_embeddings_selected_model = embedding_definition.id
|
||
SiteSetting.ai_embeddings_enabled = true
|
||
end
|
||
|
||
it "updates the tag with the suggested tag" do
|
||
response =
|
||
Tag
|
||
.take(7)
|
||
.pluck(:name)
|
||
.map { |s| { name: s, score: rand(0.0...45.0) } }
|
||
.sort { |h| h[:score] }
|
||
DiscourseAi::AiHelper::SemanticCategorizer.any_instance.stubs(:tags).returns(response)
|
||
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
|
||
ai_suggestion_dropdown.click_suggest_tags_button
|
||
|
||
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
||
|
||
suggestion = ai_suggestion_dropdown.suggestion_name(0)
|
||
ai_suggestion_dropdown.select_suggestion_by_value(0)
|
||
tag_selector = page.find(".mini-tag-chooser summary")
|
||
|
||
expect(tag_selector["data-name"]).to eq(suggestion)
|
||
end
|
||
|
||
it "does not suggest tags that already exist" do
|
||
response =
|
||
Tag
|
||
.take(7)
|
||
.pluck(:name)
|
||
.map { |s| { name: s, score: rand(0.0...45.0) } }
|
||
.sort { |h| h[:score] }
|
||
DiscourseAi::AiHelper::SemanticCategorizer.any_instance.stubs(:tags).returns(response)
|
||
|
||
topic_page.visit_topic(topic)
|
||
page.find(".edit-topic").click
|
||
page.find(".ai-tag-suggester-trigger").click
|
||
tag1_css = ".ai-tag-suggester-content btn[data-name='#{video.name}']"
|
||
tag2_css = ".ai-tag-suggester-content btn[data-name='#{music.name}']"
|
||
|
||
expect(page).to have_no_css(tag1_css)
|
||
expect(page).to have_no_css(tag2_css)
|
||
end
|
||
end
|
||
|
||
context "when AI helper is disabled" do
|
||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
||
before { SiteSetting.ai_helper_enabled = false }
|
||
|
||
it "does not show the AI helper button in the composer toolbar" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
expect(page).to have_no_css(".d-editor-button-bar button.ai-helper-trigger")
|
||
end
|
||
|
||
it "does not trigger AI suggestion buttons" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
expect(ai_suggestion_dropdown).to have_no_suggestion_button
|
||
end
|
||
end
|
||
|
||
context "when user is not a member of AI helper allowed group" do
|
||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
||
before { SiteSetting.composer_ai_helper_allowed_groups = non_member_group.id.to_s }
|
||
|
||
it "does not show the AI helper button in the composer toolbar" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
expect(page).to have_no_css(".d-editor-button-bar button.ai-helper-trigger")
|
||
end
|
||
|
||
it "does not trigger AI suggestion buttons" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
expect(ai_suggestion_dropdown).to have_no_suggestion_button
|
||
end
|
||
end
|
||
|
||
context "when suggestion features are disabled" do
|
||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
||
before { SiteSetting.ai_helper_enabled_features = "context_menu" }
|
||
|
||
it "does not show suggestion buttons in the composer" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
expect(ai_suggestion_dropdown).to have_no_suggestion_button
|
||
end
|
||
end
|
||
|
||
context "when composer helper feature is disabled" do
|
||
before { SiteSetting.ai_helper_enabled_features = "suggestions" }
|
||
|
||
it "does not show button in the composer toolbar" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
expect(page).to have_no_css(".d-editor-button-bar button.ai-helper-trigger")
|
||
end
|
||
end
|
||
|
||
context "when triggering composer AI helper", mobile: true do
|
||
it "should close the composer helper before showing the diff modal" do
|
||
visit("/latest")
|
||
page.find("#create-topic").click
|
||
composer.fill_content(input)
|
||
composer.click_toolbar_button("ai-helper-trigger")
|
||
|
||
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
||
ai_helper_menu.select_helper_model(CompletionPrompt::TRANSLATE)
|
||
expect(ai_helper_menu).to have_no_context_menu
|
||
expect(diff_modal).to be_visible
|
||
end
|
||
end
|
||
end
|
||
end
|