Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Show suggested title prompt in new location #171

Merged
merged 7 commits into from Aug 29, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,108 @@
import Component from '@glimmer/component';
import DButton from "discourse/components/d-button";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { bind } from "discourse-common/utils/decorators";

export default class AITitleSuggester extends Component {
<template>
<DButton
@class="suggest-titles-button {{if this.loading 'is-loading'}}"
@icon={{this.suggestTitleIcon}}
@title="discourse_ai.ai_helper.suggest_titles"
@action={{this.suggestTitles}}
@disabled={{this.disableSuggestionButton}}
/>
{{#if this.showMenu}}
{{! template-lint-disable modifier-name-case }}
<ul class="popup-menu ai-title-suggestions-menu" {{didInsert this.handleClickOutside}}>
{{#each this.generatedTitleSuggestions as |suggestion index|}}
<li data-name={{suggestion}} data-value={{index}}>
<DButton
@class="popup-menu-btn"
@translatedLabel={{suggestion}}
@action={{this.updateTopicTitle}}
@actionParam={{suggestion}}
/>
</li>
{{/each}}
</ul>
{{/if}}
</template>

@tracked loading = false;
@tracked showMenu = false;
@tracked generatedTitleSuggestions = [];
@tracked suggestTitleIcon = "discourse-sparkles";
mode = {
id: -2,
name: "generate_titles",
translated_name: "Suggest topic titles",
prompt_type: "list"
};

willDestroy() {
super.willDestroy(...arguments);
document.removeEventListener("click", this.onClickOutside);
}

get composerInput() {
return document.querySelector(".d-editor-input").value || this.args.outletArgs.composer.reply;
}

get disableSuggestionButton() {
return this.loading;
}

closeMenu() {
this.suggestTitleIcon = "discourse-sparkles";
this.showMenu = false;
}

@bind
onClickOutside(event) {
const menu = document.querySelector(".ai-title-suggestions-menu");

if (event.target === menu) {
return;
}

return this.closeMenu();
}

@action
handleClickOutside() {
document.addEventListener("click", this.onClickOutside);
}

@action
updateTopicTitle(title) {
const composer = this.args.outletArgs?.composer;

if (composer) {
composer.set("title", title);
this.closeMenu();
}
}

@action
async suggestTitles() {
this.loading = true;
this.suggestTitleIcon = "spinner";

return ajax("/discourse-ai/ai-helper/suggest", {
method: "POST",
data: { mode: this.mode.id, text: this.composerInput },
}).then((data) => {
this.generatedTitleSuggestions = data.suggestions;
}).catch(popupAjaxError).finally(() => {
this.loading = false;
this.suggestTitleIcon = "sync-alt";
this.showMenu = true;
});

}
}
Expand Up @@ -27,20 +27,6 @@
{{/each}}
</ul>

{{else if (eq this.menuState this.CONTEXT_MENU_STATES.suggestions)}}
<ul class="ai-helper-context-menu__suggestions">
{{#each this.generatedTitleSuggestions as |suggestion index|}}
<li data-name={{suggestion}} data-value={{index}}>
<DButton
@class="btn-flat"
@translatedLabel={{suggestion}}
@action={{this.updateTopicTitle}}
@actionParam={{suggestion}}
/>
</li>
{{/each}}
</ul>

{{else if (eq this.menuState this.CONTEXT_MENU_STATES.loading)}}
<ul class="ai-helper-context-menu__loading">
<li>
Expand Down
Expand Up @@ -42,7 +42,6 @@ export default class AiHelperContextMenu extends Component {
@tracked loading = false;
@tracked oldEditorValue;
@tracked newEditorValue;
@tracked generatedTitleSuggestions = [];
@tracked lastUsedOption = null;
@tracked showDiffModal = false;
@tracked diff;
Expand All @@ -52,7 +51,6 @@ export default class AiHelperContextMenu extends Component {
options: "OPTIONS",
resets: "RESETS",
loading: "LOADING",
suggesions: "SUGGESTIONS",
review: "REVIEW",
};
prompts = [];
Expand Down Expand Up @@ -81,21 +79,24 @@ export default class AiHelperContextMenu extends Component {
async loadPrompts() {
let prompts = await ajax("/discourse-ai/ai-helper/prompts");

prompts.map((p) => {
this.prompts[p.id] = p;
});
prompts
.filter((p) => p.name !== "generate_titles")
.map((p) => {
this.prompts[p.id] = p;
});

this.promptTypes = prompts.reduce((memo, p) => {
memo[p.name] = p.prompt_type;
return memo;
}, {});

this.helperOptions = prompts.map((p) => {
return {
name: p.translated_name,
value: p.id,
};
});
this.helperOptions = prompts
.filter((p) => p.name !== "generate_titles")
.map((p) => {
return {
name: p.translated_name,
value: p.id,
};
});
}

@bind
Expand Down Expand Up @@ -297,13 +298,7 @@ export default class AiHelperContextMenu extends Component {
// resets the values if new suggestion is started:
this.diff = null;
this.newSelectedText = null;

if (this.prompts[option].name === "generate_titles") {
this.menuState = this.CONTEXT_MENU_STATES.suggestions;
this.generatedTitleSuggestions = data.suggestions;
} else {
this._updateSuggestedByAI(data);
}
this._updateSuggestedByAI(data);
})
.catch(popupAjaxError)
.finally(() => {
Expand All @@ -312,16 +307,6 @@ export default class AiHelperContextMenu extends Component {
});
}

@action
updateTopicTitle(title) {
const composer = this.args.outletArgs?.composer;

if (composer) {
composer.set("title", title);
this.closeContextMenu();
}
}

@action
viewChanges() {
this.showDiffModal = true;
Expand Down
33 changes: 33 additions & 0 deletions assets/stylesheets/modules/ai-helper/common/ai-helper.scss
Expand Up @@ -209,3 +209,36 @@
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
}
}

// Suggest Titles Related
.suggest-titles-button {
position: absolute;
top: 1px;
right: 1px;
background: none;
border: none;

.d-icon-spinner {
animation: spin 1s linear infinite;
}
}

.ai-title-suggestions-menu {
list-style: none;
margin-left: 0;
position: absolute;
right: 0;
top: 1.5rem;
max-width: 25rem;
width: unset;
z-index: 999;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Expand Up @@ -16,6 +16,7 @@ en:
title: "Suggest changes using AI"
description: "Choose one of the options below, and the AI will suggest you a new version of the text."
selection_hint: "Hint: You can also select a portion of the text before opening the helper to rewrite only that."
suggest_titles: "Suggest titles with AI"
context_menu:
trigger: "AI"
undo: "Undo"
Expand Down
54 changes: 39 additions & 15 deletions spec/system/ai_helper/ai_composer_helper_spec.rb
Expand Up @@ -15,6 +15,7 @@
let(:ai_helper_context_menu) { PageObjects::Components::AIHelperContextMenu.new }
let(:ai_helper_modal) { PageObjects::Modals::AiHelper.new }
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
let(:ai_title_suggester) { PageObjects::Components::AITitleSuggester.new }

context "when using the translation mode" do
let(:mode) { OpenAiCompletionsInferenceStubs::TRANSLATE }
Expand Down Expand Up @@ -285,25 +286,48 @@ def trigger_context_menu(content)
)
end
end
end

context "when selecting an AI generated title" do
let(:mode) { OpenAiCompletionsInferenceStubs::GENERATE_TITLES }
before { OpenAiCompletionsInferenceStubs.stub_prompt(mode) }
context "when suggesting titles with AI title suggester" do
let(:mode) { OpenAiCompletionsInferenceStubs::GENERATE_TITLES }
before { OpenAiCompletionsInferenceStubs.stub_prompt(mode) }

it "replaces the topic title" do
trigger_context_menu(OpenAiCompletionsInferenceStubs.translated_response)
ai_helper_context_menu.click_ai_button
ai_helper_context_menu.select_helper_model(
OpenAiCompletionsInferenceStubs.text_mode_to_id(mode),
)
expect(ai_helper_context_menu).to be_showing_suggestions
it "opens a menu with title suggestions" do
visit("/latest")
page.find("#create-topic").click
composer.fill_content(OpenAiCompletionsInferenceStubs.translated_response)
ai_title_suggester.click_suggest_titles_button

ai_helper_context_menu.select_title_suggestion(2)
expected_title = "The Quiet Piece that Moves Literature: A Gaucho's Story"
wait_for { ai_title_suggester.has_dropdown? }

wait_for { find("#reply-title").value == expected_title }
expect(find("#reply-title").value).to eq(expected_title)
end
expect(ai_title_suggester).to have_dropdown
end

it "replaces the topic title with the selected title" do
visit("/latest")
page.find("#create-topic").click
composer.fill_content(OpenAiCompletionsInferenceStubs.translated_response)
ai_title_suggester.click_suggest_titles_button

wait_for { ai_title_suggester.has_dropdown? }

ai_title_suggester.select_title_suggestion(2)
expected_title = "The Quiet Piece that Moves Literature: A Gaucho's Story"

expect(find("#reply-title").value).to eq(expected_title)
end

it "closes the menu when clicking outside" do
visit("/latest")
page.find("#create-topic").click
composer.fill_content(OpenAiCompletionsInferenceStubs.translated_response)
ai_title_suggester.click_suggest_titles_button

wait_for { ai_title_suggester.has_dropdown? }

find(".d-editor-preview").click

expect(ai_title_suggester).to have_no_dropdown
end
end
end
9 changes: 0 additions & 9 deletions spec/system/page_objects/components/ai_helper_context_menu.rb
Expand Up @@ -7,7 +7,6 @@ class AIHelperContextMenu < PageObjects::Components::Base
CONTEXT_MENU_SELECTOR = ".ai-helper-context-menu"
TRIGGER_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__trigger"
OPTIONS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__options"
SUGGESTIONS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__suggestions"
LOADING_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__loading"
RESETS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__resets"
REVIEW_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__review"
Expand All @@ -20,10 +19,6 @@ def select_helper_model(mode)
find("#{OPTIONS_STATE_SELECTOR} li[data-value=\"#{mode}\"] .btn").click
end

def select_title_suggestion(option_number)
find("#{SUGGESTIONS_STATE_SELECTOR} li[data-value=\"#{option_number}\"] .btn").click
end

def click_undo_button
find("#{RESETS_STATE_SELECTOR} .undo").click
end
Expand Down Expand Up @@ -64,10 +59,6 @@ def showing_options?
page.has_css?(OPTIONS_STATE_SELECTOR)
end

def showing_suggestions?
page.has_css?(SUGGESTIONS_STATE_SELECTOR)
end

def showing_loading?
page.has_css?(LOADING_STATE_SELECTOR)
end
Expand Down
26 changes: 26 additions & 0 deletions spec/system/page_objects/components/ai_title_suggester.rb
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module PageObjects
module Components
class AITitleSuggester < PageObjects::Components::Base
BUTTON_SELECTOR = ".suggest-titles-button"
MENU_SELECTOR = ".ai-title-suggestions-menu"

def click_suggest_titles_button
find(BUTTON_SELECTOR, visible: :all).click
end

def select_title_suggestion(index)
find("#{MENU_SELECTOR} li[data-value=\"#{index}\"]").click
end

def has_dropdown?
has_css?(MENU_SELECTOR)
end

def has_no_dropdown?
has_no_css?(MENU_SELECTOR)
end
end
end
end