FEATURE: Show suggested title prompt in new location (#171)
This commit is contained in:
parent
345bfed19f
commit
7457feced8
|
@ -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;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
@ -52,7 +51,6 @@ export default class AiHelperContextMenu extends Component {
|
|||
options: "OPTIONS",
|
||||
resets: "RESETS",
|
||||
loading: "LOADING",
|
||||
suggesions: "SUGGESTIONS",
|
||||
review: "REVIEW",
|
||||
};
|
||||
prompts = [];
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -15,6 +15,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
|||
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 }
|
||||
|
@ -285,25 +286,48 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
|||
)
|
||||
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
|
||||
|
|
|
@ -7,7 +7,6 @@ module PageObjects
|
|||
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"
|
||||
|
@ -20,10 +19,6 @@ module PageObjects
|
|||
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
|
||||
|
@ -64,10 +59,6 @@ module PageObjects
|
|||
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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue