mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-28 18:42:16 +00:00
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}}
|
{{/each}}
|
||||||
</ul>
|
</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)}}
|
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.loading)}}
|
||||||
<ul class="ai-helper-context-menu__loading">
|
<ul class="ai-helper-context-menu__loading">
|
||||||
<li>
|
<li>
|
||||||
|
@ -42,7 +42,6 @@ export default class AiHelperContextMenu extends Component {
|
|||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
@tracked oldEditorValue;
|
@tracked oldEditorValue;
|
||||||
@tracked newEditorValue;
|
@tracked newEditorValue;
|
||||||
@tracked generatedTitleSuggestions = [];
|
|
||||||
@tracked lastUsedOption = null;
|
@tracked lastUsedOption = null;
|
||||||
@tracked showDiffModal = false;
|
@tracked showDiffModal = false;
|
||||||
@tracked diff;
|
@tracked diff;
|
||||||
@ -52,7 +51,6 @@ export default class AiHelperContextMenu extends Component {
|
|||||||
options: "OPTIONS",
|
options: "OPTIONS",
|
||||||
resets: "RESETS",
|
resets: "RESETS",
|
||||||
loading: "LOADING",
|
loading: "LOADING",
|
||||||
suggesions: "SUGGESTIONS",
|
|
||||||
review: "REVIEW",
|
review: "REVIEW",
|
||||||
};
|
};
|
||||||
prompts = [];
|
prompts = [];
|
||||||
@ -81,7 +79,9 @@ export default class AiHelperContextMenu extends Component {
|
|||||||
async loadPrompts() {
|
async loadPrompts() {
|
||||||
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
|
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
|
||||||
|
|
||||||
prompts.map((p) => {
|
prompts
|
||||||
|
.filter((p) => p.name !== "generate_titles")
|
||||||
|
.map((p) => {
|
||||||
this.prompts[p.id] = p;
|
this.prompts[p.id] = p;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -89,8 +89,9 @@ export default class AiHelperContextMenu extends Component {
|
|||||||
memo[p.name] = p.prompt_type;
|
memo[p.name] = p.prompt_type;
|
||||||
return memo;
|
return memo;
|
||||||
}, {});
|
}, {});
|
||||||
|
this.helperOptions = prompts
|
||||||
this.helperOptions = prompts.map((p) => {
|
.filter((p) => p.name !== "generate_titles")
|
||||||
|
.map((p) => {
|
||||||
return {
|
return {
|
||||||
name: p.translated_name,
|
name: p.translated_name,
|
||||||
value: p.id,
|
value: p.id,
|
||||||
@ -297,13 +298,7 @@ export default class AiHelperContextMenu extends Component {
|
|||||||
// resets the values if new suggestion is started:
|
// resets the values if new suggestion is started:
|
||||||
this.diff = null;
|
this.diff = null;
|
||||||
this.newSelectedText = 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)
|
.catch(popupAjaxError)
|
||||||
.finally(() => {
|
.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
|
@action
|
||||||
viewChanges() {
|
viewChanges() {
|
||||||
this.showDiffModal = true;
|
this.showDiffModal = true;
|
||||||
|
@ -209,3 +209,36 @@
|
|||||||
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
|
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"
|
title: "Suggest changes using AI"
|
||||||
description: "Choose one of the options below, and the AI will suggest you a new version of the text."
|
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."
|
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:
|
context_menu:
|
||||||
trigger: "AI"
|
trigger: "AI"
|
||||||
undo: "Undo"
|
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_context_menu) { PageObjects::Components::AIHelperContextMenu.new }
|
||||||
let(:ai_helper_modal) { PageObjects::Modals::AiHelper.new }
|
let(:ai_helper_modal) { PageObjects::Modals::AiHelper.new }
|
||||||
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
|
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
|
||||||
|
let(:ai_title_suggester) { PageObjects::Components::AITitleSuggester.new }
|
||||||
|
|
||||||
context "when using the translation mode" do
|
context "when using the translation mode" do
|
||||||
let(:mode) { OpenAiCompletionsInferenceStubs::TRANSLATE }
|
let(:mode) { OpenAiCompletionsInferenceStubs::TRANSLATE }
|
||||||
@ -285,25 +286,48 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when selecting an AI generated title" do
|
context "when suggesting titles with AI title suggester" do
|
||||||
let(:mode) { OpenAiCompletionsInferenceStubs::GENERATE_TITLES }
|
let(:mode) { OpenAiCompletionsInferenceStubs::GENERATE_TITLES }
|
||||||
before { OpenAiCompletionsInferenceStubs.stub_prompt(mode) }
|
before { OpenAiCompletionsInferenceStubs.stub_prompt(mode) }
|
||||||
|
|
||||||
it "replaces the topic title" do
|
it "opens a menu with title suggestions" do
|
||||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.translated_response)
|
visit("/latest")
|
||||||
ai_helper_context_menu.click_ai_button
|
page.find("#create-topic").click
|
||||||
ai_helper_context_menu.select_helper_model(
|
composer.fill_content(OpenAiCompletionsInferenceStubs.translated_response)
|
||||||
OpenAiCompletionsInferenceStubs.text_mode_to_id(mode),
|
ai_title_suggester.click_suggest_titles_button
|
||||||
)
|
|
||||||
expect(ai_helper_context_menu).to be_showing_suggestions
|
|
||||||
|
|
||||||
ai_helper_context_menu.select_title_suggestion(2)
|
wait_for { ai_title_suggester.has_dropdown? }
|
||||||
|
|
||||||
|
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"
|
expected_title = "The Quiet Piece that Moves Literature: A Gaucho's Story"
|
||||||
|
|
||||||
wait_for { find("#reply-title").value == expected_title }
|
|
||||||
expect(find("#reply-title").value).to eq(expected_title)
|
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(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
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,7 +7,6 @@ module PageObjects
|
|||||||
CONTEXT_MENU_SELECTOR = ".ai-helper-context-menu"
|
CONTEXT_MENU_SELECTOR = ".ai-helper-context-menu"
|
||||||
TRIGGER_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__trigger"
|
TRIGGER_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__trigger"
|
||||||
OPTIONS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__options"
|
OPTIONS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__options"
|
||||||
SUGGESTIONS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__suggestions"
|
|
||||||
LOADING_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__loading"
|
LOADING_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__loading"
|
||||||
RESETS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__resets"
|
RESETS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__resets"
|
||||||
REVIEW_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__review"
|
REVIEW_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__review"
|
||||||
@ -20,10 +19,6 @@ module PageObjects
|
|||||||
find("#{OPTIONS_STATE_SELECTOR} li[data-value=\"#{mode}\"] .btn").click
|
find("#{OPTIONS_STATE_SELECTOR} li[data-value=\"#{mode}\"] .btn").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_title_suggestion(option_number)
|
|
||||||
find("#{SUGGESTIONS_STATE_SELECTOR} li[data-value=\"#{option_number}\"] .btn").click
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_undo_button
|
def click_undo_button
|
||||||
find("#{RESETS_STATE_SELECTOR} .undo").click
|
find("#{RESETS_STATE_SELECTOR} .undo").click
|
||||||
end
|
end
|
||||||
@ -64,10 +59,6 @@ module PageObjects
|
|||||||
page.has_css?(OPTIONS_STATE_SELECTOR)
|
page.has_css?(OPTIONS_STATE_SELECTOR)
|
||||||
end
|
end
|
||||||
|
|
||||||
def showing_suggestions?
|
|
||||||
page.has_css?(SUGGESTIONS_STATE_SELECTOR)
|
|
||||||
end
|
|
||||||
|
|
||||||
def showing_loading?
|
def showing_loading?
|
||||||
page.has_css?(LOADING_STATE_SELECTOR)
|
page.has_css?(LOADING_STATE_SELECTOR)
|
||||||
end
|
end
|
||||||
|
26
spec/system/page_objects/components/ai_title_suggester.rb
Normal file
26
spec/system/page_objects/components/ai_title_suggester.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user