FEATURE: Show suggested title prompt in new location (#171)

This commit is contained in:
Keegan George 2023-08-29 09:45:53 -07:00 committed by GitHub
parent 345bfed19f
commit 7457feced8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 221 additions and 67 deletions

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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