mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-02-21 10:55:34 +00:00
FEATURE: AI Helper Context Menu (#148)
This commit is contained in:
parent
f0e1c72aa7
commit
6df850d473
@ -0,0 +1,78 @@
|
||||
<div {{did-insert this.setupContextMenu}}>
|
||||
{{#if this.showContextMenu}}
|
||||
<div class="ai-helper-context-menu">
|
||||
{{#if (eq this.menuState this.CONTEXT_MENU_STATES.triggers)}}
|
||||
<ul class="ai-helper-context-menu__trigger">
|
||||
<li>
|
||||
<DButton
|
||||
@icon="magic"
|
||||
@action={{this.toggleAiHelperOptions}}
|
||||
@label="discourse_ai.ai_helper.context_menu.trigger"
|
||||
class="btn-flat"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.options)}}
|
||||
<ul class="ai-helper-context-menu__options">
|
||||
{{#each this.helperOptions as |option|}}
|
||||
<li data-name={{option.name}} data-value={{option.value}}>
|
||||
<DButton
|
||||
@class="btn-flat"
|
||||
@translatedLabel={{option.name}}
|
||||
@action={{this.updateSelected}}
|
||||
@actionParam={{option.value}}
|
||||
/>
|
||||
</li>
|
||||
{{/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>
|
||||
<div class="dot-falling"></div>
|
||||
<span>
|
||||
{{i18n "discourse_ai.ai_helper.context_menu.loading"}}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.resets)}}
|
||||
<ul class="ai-helper-context-menu__resets">
|
||||
<li>
|
||||
<DButton
|
||||
@icon="undo"
|
||||
@label="discourse_ai.ai_helper.context_menu.undo"
|
||||
@action={{this.undoAIAction}}
|
||||
class="btn-flat undo"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<DButton
|
||||
@icon="discourse-sparkles"
|
||||
@label="discourse_ai.ai_helper.context_menu.regen"
|
||||
@action={{this.updateSelected}}
|
||||
@actionParam={{this.lastUsedOption}}
|
||||
class="btn-flat"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
@ -0,0 +1,233 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { afterRender, bind, debounce } from "discourse-common/utils/decorators";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import { caretPosition, getCaretPosition } from "discourse/lib/utilities";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class AiHelperContextMenu extends Component {
|
||||
static shouldRender(outletArgs, helper) {
|
||||
return (
|
||||
helper.siteSettings.discourse_ai_enabled &&
|
||||
helper.siteSettings.composer_ai_helper_enabled
|
||||
);
|
||||
}
|
||||
|
||||
@service siteSettings;
|
||||
@tracked helperOptions = [];
|
||||
@tracked showContextMenu = false;
|
||||
@tracked menuState = this.CONTEXT_MENU_STATES.triggers;
|
||||
@tracked caretCoords;
|
||||
@tracked virtualElement;
|
||||
@tracked selectedText = "";
|
||||
@tracked loading = false;
|
||||
@tracked oldEditorValue;
|
||||
@tracked generatedTitleSuggestions = [];
|
||||
@tracked lastUsedOption = null;
|
||||
|
||||
CONTEXT_MENU_STATES = {
|
||||
triggers: "TRIGGERS",
|
||||
options: "OPTIONS",
|
||||
resets: "RESETS",
|
||||
loading: "LOADING",
|
||||
suggesions: "SUGGESTIONS",
|
||||
};
|
||||
prompts = [];
|
||||
promptTypes = {};
|
||||
|
||||
@tracked _popper;
|
||||
@tracked _dEditorInput;
|
||||
@tracked _contextMenu;
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
document.removeEventListener("selectionchange", this.selectionChanged);
|
||||
this._popper?.destroy();
|
||||
}
|
||||
|
||||
async loadPrompts() {
|
||||
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
|
||||
|
||||
prompts.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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
selectionChanged(event) {
|
||||
if (!event.target.activeElement.classList.contains("d-editor-input")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.getSelection().toString().length === 0) {
|
||||
if (this.loading) {
|
||||
// prevent accidentally closing context menu while results loading
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedText = event.target.getSelection().toString();
|
||||
this._onSelectionChanged();
|
||||
}
|
||||
|
||||
@bind
|
||||
updatePosition() {
|
||||
if (!this.showContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.positionContextMenu();
|
||||
}
|
||||
|
||||
@debounce(INPUT_DELAY)
|
||||
_onSelectionChanged() {
|
||||
this.positionContextMenu();
|
||||
this.showContextMenu = true;
|
||||
}
|
||||
|
||||
generateGetBoundingClientRect(width = 0, height = 0, x = 0, y = 0) {
|
||||
return () => ({
|
||||
width,
|
||||
height,
|
||||
top: y,
|
||||
right: x,
|
||||
bottom: y,
|
||||
left: x,
|
||||
});
|
||||
}
|
||||
|
||||
closeContextMenu() {
|
||||
this.showContextMenu = false;
|
||||
this.menuState = this.CONTEXT_MENU_STATES.triggers;
|
||||
}
|
||||
|
||||
_updateSuggestedByAI(data) {
|
||||
const composer = this.args.outletArgs.composer;
|
||||
this.oldEditorValue = this._dEditorInput.value;
|
||||
const newValue = this.oldEditorValue.replace(
|
||||
this.selectedText,
|
||||
data.suggestions[0]
|
||||
);
|
||||
composer.set("reply", newValue);
|
||||
this.menuState = this.CONTEXT_MENU_STATES.resets;
|
||||
}
|
||||
|
||||
@afterRender
|
||||
positionContextMenu() {
|
||||
this._contextMenu = document.querySelector(".ai-helper-context-menu");
|
||||
this.caretCoords = getCaretPosition(this._dEditorInput, {
|
||||
pos: caretPosition(this._dEditorInput),
|
||||
});
|
||||
|
||||
this.virtualElement = {
|
||||
getBoundingClientRect: this.generateGetBoundingClientRect(
|
||||
this._contextMenu.clientWidth,
|
||||
this._contextMenu.clientHeight,
|
||||
this.caretCoords.x,
|
||||
this.caretCoords.y
|
||||
),
|
||||
};
|
||||
|
||||
this._popper = createPopper(this.virtualElement, this._contextMenu, {
|
||||
placement: "top-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [10, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
setupContextMenu() {
|
||||
document.addEventListener("selectionchange", this.selectionChanged);
|
||||
|
||||
this._dEditorInput = document.querySelector(".d-editor-input");
|
||||
|
||||
if (this._dEditorInput) {
|
||||
this._dEditorInput.addEventListener("scroll", this.updatePosition);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleAiHelperOptions() {
|
||||
// Fetch prompts only if it hasn't been fetched yet
|
||||
if (this.helperOptions.length === 0) {
|
||||
this.loadPrompts();
|
||||
}
|
||||
this.menuState = this.CONTEXT_MENU_STATES.options;
|
||||
}
|
||||
|
||||
@action
|
||||
undoAIAction() {
|
||||
const composer = this.args.outletArgs.composer;
|
||||
composer.set("reply", this.oldEditorValue);
|
||||
this.closeContextMenu();
|
||||
}
|
||||
|
||||
@action
|
||||
async updateSelected(option) {
|
||||
this.loading = true;
|
||||
this.lastUsedOption = option;
|
||||
this._dEditorInput.classList.add("loading");
|
||||
this.menuState = this.CONTEXT_MENU_STATES.loading;
|
||||
|
||||
return ajax("/discourse-ai/ai-helper/suggest", {
|
||||
method: "POST",
|
||||
data: { mode: option, text: this.selectedText },
|
||||
})
|
||||
.then((data) => {
|
||||
if (this.prompts[option].name === "generate_titles") {
|
||||
this.menuState = this.CONTEXT_MENU_STATES.suggestions;
|
||||
this.generatedTitleSuggestions = data.suggestions;
|
||||
} else {
|
||||
this._updateSuggestedByAI(data);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this._dEditorInput.classList.remove("loading");
|
||||
|
||||
// Make reset options disappear by closing the context menu after 5 seconds
|
||||
if (this.menuState === this.CONTEXT_MENU_STATES.resets) {
|
||||
discourseLater(() => {
|
||||
this.closeContextMenu();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
updateTopicTitle(title) {
|
||||
const composer = this.args.outletArgs?.composer;
|
||||
|
||||
if (composer) {
|
||||
composer.set("title", title);
|
||||
this.closeContextMenu();
|
||||
}
|
||||
}
|
||||
}
|
@ -29,3 +29,149 @@
|
||||
.topic-above-suggested-outlet.related-topics {
|
||||
margin: 4.5em 0 1em;
|
||||
}
|
||||
|
||||
.ai-helper-context-menu {
|
||||
background: var(--secondary);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
padding: 0.25rem;
|
||||
max-width: 15rem;
|
||||
border: 1px solid var(--primary-low);
|
||||
list-style: none;
|
||||
z-index: 999;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul:not(.ai-helper-context-menu__loading) li {
|
||||
transition: background-color 0.25s ease;
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
.d-button-label {
|
||||
color: var(--primary-very-high);
|
||||
}
|
||||
|
||||
&__options {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
.dot-falling {
|
||||
margin-inline: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
gap: 1rem;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__resets {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.d-editor-input.loading {
|
||||
animation: loading-text 1.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes loading-text {
|
||||
0% {
|
||||
color: var(--primary);
|
||||
}
|
||||
50% {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
100% {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
// AI Typing indicator (taken from: https://github.com/nzbin/three-dots)
|
||||
.dot-falling {
|
||||
position: relative;
|
||||
left: -9999px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--tertiary);
|
||||
color: var(--tertiary);
|
||||
box-shadow: 9999px 0 0 0 var(--tertiary);
|
||||
animation: dot-falling 1s infinite linear;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.dot-falling::before,
|
||||
.dot-falling::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.dot-falling::before {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--tertiary);
|
||||
color: var(--tertiary);
|
||||
animation: dot-falling-before 1s infinite linear;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.dot-falling::after {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--tertiary);
|
||||
color: var(--tertiary);
|
||||
animation: dot-falling-after 1s infinite linear;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
@keyframes dot-falling {
|
||||
0% {
|
||||
box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
|
||||
}
|
||||
25%,
|
||||
50%,
|
||||
75% {
|
||||
box-shadow: 9999px 0 0 0 var(--tertiary);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
|
||||
}
|
||||
}
|
||||
@keyframes dot-falling-before {
|
||||
0% {
|
||||
box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
|
||||
}
|
||||
25%,
|
||||
50%,
|
||||
75% {
|
||||
box-shadow: 9984px 0 0 0 var(--tertiary);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
|
||||
}
|
||||
}
|
||||
@keyframes dot-falling-after {
|
||||
0% {
|
||||
box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
|
||||
}
|
||||
25%,
|
||||
50%,
|
||||
75% {
|
||||
box-shadow: 10014px 0 0 0 var(--tertiary);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,12 @@ 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."
|
||||
context_menu:
|
||||
trigger: "AI"
|
||||
undo: "Undo"
|
||||
loading: "AI is generating"
|
||||
cancel: "Cancel"
|
||||
regen: "Try Again"
|
||||
reviewables:
|
||||
model_used: "Model used:"
|
||||
accuracy: "Accuracy:"
|
||||
@ -35,7 +41,6 @@ en:
|
||||
5-turbo: "GPT-3.5"
|
||||
claude-2: "Claude 2"
|
||||
|
||||
|
||||
review:
|
||||
types:
|
||||
reviewable_ai_post:
|
||||
|
@ -12,6 +12,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||
end
|
||||
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
let(:ai_helper_context_menu) { PageObjects::Components::AIHelperContextMenu.new }
|
||||
let(:ai_helper_modal) { PageObjects::Modals::AiHelper.new }
|
||||
|
||||
context "when using the translation mode" do
|
||||
@ -83,4 +84,136 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||
expect(find("#reply-title").value).to eq(expected_title)
|
||||
end
|
||||
end
|
||||
|
||||
def trigger_context_menu(content)
|
||||
visit("/latest")
|
||||
page.find("#create-topic").click
|
||||
composer.fill_content(content)
|
||||
page.execute_script("document.querySelector('.d-editor-input')?.select();")
|
||||
end
|
||||
|
||||
context "when triggering AI with context menu in composer" do
|
||||
it "shows the context menu when selecting a passage of text in the composer" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.translated_response)
|
||||
expect(ai_helper_context_menu).to have_context_menu
|
||||
end
|
||||
|
||||
it "shows context menu in 'trigger' state when first showing" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.translated_response)
|
||||
expect(ai_helper_context_menu).to be_showing_triggers
|
||||
end
|
||||
|
||||
it "shows prompt options in context menu when AI button is clicked" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.translated_response)
|
||||
ai_helper_context_menu.click_ai_button
|
||||
expect(ai_helper_context_menu).to be_showing_options
|
||||
end
|
||||
|
||||
context "when using translation mode" do
|
||||
let(:mode) { OpenAiCompletionsInferenceStubs::TRANSLATE }
|
||||
before { OpenAiCompletionsInferenceStubs.stub_prompt(mode) }
|
||||
|
||||
it "replaces the composed message with AI generated content" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.spanish_text)
|
||||
ai_helper_context_menu.click_ai_button
|
||||
ai_helper_context_menu.select_helper_model(
|
||||
OpenAiCompletionsInferenceStubs.text_mode_to_id(mode),
|
||||
)
|
||||
|
||||
wait_for do
|
||||
composer.composer_input.value == OpenAiCompletionsInferenceStubs.translated_response.strip
|
||||
end
|
||||
|
||||
expect(composer.composer_input.value).to eq(
|
||||
OpenAiCompletionsInferenceStubs.translated_response.strip,
|
||||
)
|
||||
end
|
||||
|
||||
it "shows reset options after results are complete" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.spanish_text)
|
||||
ai_helper_context_menu.click_ai_button
|
||||
ai_helper_context_menu.select_helper_model(
|
||||
OpenAiCompletionsInferenceStubs.text_mode_to_id(mode),
|
||||
)
|
||||
|
||||
wait_for do
|
||||
composer.composer_input.value == OpenAiCompletionsInferenceStubs.translated_response.strip
|
||||
end
|
||||
|
||||
expect(ai_helper_context_menu).to be_showing_resets
|
||||
end
|
||||
|
||||
it "hides reset options after 5 seconds" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.spanish_text)
|
||||
ai_helper_context_menu.click_ai_button
|
||||
ai_helper_context_menu.select_helper_model(
|
||||
OpenAiCompletionsInferenceStubs.text_mode_to_id(mode),
|
||||
)
|
||||
|
||||
wait_for do
|
||||
composer.composer_input.value == OpenAiCompletionsInferenceStubs.translated_response.strip
|
||||
end
|
||||
|
||||
expect(ai_helper_context_menu).to be_showing_resets
|
||||
sleep 5
|
||||
expect(ai_helper_context_menu).to be_not_showing_resets
|
||||
end
|
||||
|
||||
it "reverts results when Undo button is clicked" do
|
||||
trigger_context_menu(OpenAiCompletionsInferenceStubs.spanish_text)
|
||||
ai_helper_context_menu.click_ai_button
|
||||
ai_helper_context_menu.select_helper_model(
|
||||
OpenAiCompletionsInferenceStubs.text_mode_to_id(mode),
|
||||
)
|
||||
|
||||
wait_for do
|
||||
composer.composer_input.value == OpenAiCompletionsInferenceStubs.translated_response.strip
|
||||
end
|
||||
|
||||
ai_helper_context_menu.click_undo_button
|
||||
expect(composer.composer_input.value).to eq(OpenAiCompletionsInferenceStubs.spanish_text)
|
||||
end
|
||||
end
|
||||
|
||||
context "when using the proofreading mode" do
|
||||
let(:mode) { OpenAiCompletionsInferenceStubs::PROOFREAD }
|
||||
before { OpenAiCompletionsInferenceStubs.stub_prompt(mode) }
|
||||
|
||||
it "replaces the composed message with AI generated content" 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),
|
||||
)
|
||||
|
||||
wait_for do
|
||||
composer.composer_input.value == OpenAiCompletionsInferenceStubs.proofread_response.strip
|
||||
end
|
||||
|
||||
expect(composer.composer_input.value).to eq(
|
||||
OpenAiCompletionsInferenceStubs.proofread_response.strip,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when selecting an AI generated title" 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
|
||||
|
||||
ai_helper_context_menu.select_title_suggestion(2)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Components
|
||||
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"
|
||||
|
||||
def click_ai_button
|
||||
find("#{TRIGGER_STATE_SELECTOR} .btn").click
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def has_context_menu?
|
||||
page.has_css?(CONTEXT_MENU_SELECTOR)
|
||||
end
|
||||
|
||||
def showing_triggers?
|
||||
page.has_css?(TRIGGER_STATE_SELECTOR)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def showing_resets?
|
||||
page.has_css?(RESETS_STATE_SELECTOR)
|
||||
end
|
||||
|
||||
def not_showing_resets?
|
||||
page.has_no_css?(RESETS_STATE_SELECTOR)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user