UX: Highlight AI post helper selection (#520)
This commit is contained in:
parent
fc6b937df7
commit
cb4d438506
|
@ -40,6 +40,8 @@ export default class AIHelperOptionsMenu extends Component {
|
||||||
@tracked copyButtonLabel = "discourse_ai.ai_helper.post_options_menu.copy";
|
@tracked copyButtonLabel = "discourse_ai.ai_helper.post_options_menu.copy";
|
||||||
@tracked showFastEdit = false;
|
@tracked showFastEdit = false;
|
||||||
@tracked showAiButtons = true;
|
@tracked showAiButtons = true;
|
||||||
|
@tracked originalPostHTML = null;
|
||||||
|
@tracked postHighlighted = false;
|
||||||
|
|
||||||
MENU_STATES = {
|
MENU_STATES = {
|
||||||
triggers: "TRIGGERS",
|
triggers: "TRIGGERS",
|
||||||
|
@ -50,8 +52,99 @@ export default class AIHelperOptionsMenu extends Component {
|
||||||
|
|
||||||
@tracked _activeAIRequest = null;
|
@tracked _activeAIRequest = null;
|
||||||
|
|
||||||
|
highlightSelectedText() {
|
||||||
|
const postId = this.args.outletArgs.data.quoteState.postId;
|
||||||
|
const postElement = document.querySelector(
|
||||||
|
`article[data-post-id='${postId}']`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!postElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.originalPostHTML = postElement.innerHTML;
|
||||||
|
this.selectedText = this.args.outletArgs.data.quoteState.buffer;
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection.rangeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
|
||||||
|
// Split start/end text nodes at their range boundary
|
||||||
|
if (
|
||||||
|
range.startContainer.nodeType === Node.TEXT_NODE &&
|
||||||
|
range.startOffset > 0
|
||||||
|
) {
|
||||||
|
const newStartNode = range.startContainer.splitText(range.startOffset);
|
||||||
|
range.setStart(newStartNode, 0);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
range.endContainer.nodeType === Node.TEXT_NODE &&
|
||||||
|
range.endOffset < range.endContainer.length
|
||||||
|
) {
|
||||||
|
range.endContainer.splitText(range.endOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Walker to traverse text nodes within range
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
range.commonAncestorContainer,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
{
|
||||||
|
acceptNode: (node) =>
|
||||||
|
range.intersectsNode(node)
|
||||||
|
? NodeFilter.FILTER_ACCEPT
|
||||||
|
: NodeFilter.FILTER_REJECT,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const textNodes = [];
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
textNodes.push(walker.currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let textNode of textNodes) {
|
||||||
|
const highlight = document.createElement("span");
|
||||||
|
highlight.classList.add("ai-helper-highlighted-selection");
|
||||||
|
|
||||||
|
// Replace textNode with highlighted clone
|
||||||
|
const clone = textNode.cloneNode(true);
|
||||||
|
highlight.appendChild(clone);
|
||||||
|
|
||||||
|
textNode.parentNode.replaceChild(highlight, textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.removeAllRanges();
|
||||||
|
this.postHighlighted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHighlightedText() {
|
||||||
|
if (!this.postHighlighted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postId = this.args.outletArgs.data.quoteState.postId;
|
||||||
|
const postElement = document.querySelector(
|
||||||
|
`article[data-post-id='${postId}']`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!postElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postElement.innerHTML = this.originalPostHTML;
|
||||||
|
this.postHighlighted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
this.removeHighlightedText();
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async showAIHelperOptions() {
|
async showAIHelperOptions() {
|
||||||
|
this.highlightSelectedText();
|
||||||
this.showMainButtons = false;
|
this.showMainButtons = false;
|
||||||
this.menuState = this.MENU_STATES.options;
|
this.menuState = this.MENU_STATES.options;
|
||||||
}
|
}
|
||||||
|
@ -75,6 +168,19 @@ export default class AIHelperOptionsMenu extends Component {
|
||||||
this.suggestion = result.result;
|
this.suggestion = result.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get highlightedTextToggleIcon() {
|
||||||
|
if (this.showHighlightedText) {
|
||||||
|
return "angle-double-left";
|
||||||
|
} else {
|
||||||
|
return "angle-double-right";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleHighlightedTextPreview() {
|
||||||
|
this.showHighlightedText = !this.showHighlightedText;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async performAISuggestion(option) {
|
async performAISuggestion(option) {
|
||||||
this.menuState = this.MENU_STATES.loading;
|
this.menuState = this.MENU_STATES.loading;
|
||||||
|
|
|
@ -141,6 +141,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-helper-highlighted-selection {
|
||||||
|
color: var(--tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
// AI Typing indicator (taken from: https://github.com/nzbin/three-dots)
|
// AI Typing indicator (taken from: https://github.com/nzbin/three-dots)
|
||||||
.dot-falling {
|
.dot-falling {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -57,6 +57,14 @@ RSpec.describe "AI Post helper", type: :system, js: true do
|
||||||
expect(post_ai_helper).to have_post_ai_helper_options
|
expect(post_ai_helper).to have_post_ai_helper_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "highlights the selected text after clicking the AI button and removes after closing" do
|
||||||
|
select_post_text(post)
|
||||||
|
post_ai_helper.click_ai_button
|
||||||
|
expect(post_ai_helper).to have_highlighted_text
|
||||||
|
find("article[data-post-id='#{post.id}']").click
|
||||||
|
expect(post_ai_helper).to have_no_highlighted_text
|
||||||
|
end
|
||||||
|
|
||||||
context "when using explain mode" do
|
context "when using explain mode" do
|
||||||
let(:mode) { CompletionPrompt::EXPLAIN }
|
let(:mode) { CompletionPrompt::EXPLAIN }
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ module PageObjects
|
||||||
OPTIONS_SELECTOR = "#{AI_HELPER_SELECTOR}__options"
|
OPTIONS_SELECTOR = "#{AI_HELPER_SELECTOR}__options"
|
||||||
LOADING_SELECTOR = ".ai-helper-context-menu__loading"
|
LOADING_SELECTOR = ".ai-helper-context-menu__loading"
|
||||||
SUGGESTION_SELECTOR = "#{AI_HELPER_SELECTOR}__suggestion"
|
SUGGESTION_SELECTOR = "#{AI_HELPER_SELECTOR}__suggestion"
|
||||||
|
HIGHLIGHT_SELECTOR = ".ai-helper-highlighted-selection"
|
||||||
|
|
||||||
def click_ai_button
|
def click_ai_button
|
||||||
find(TRIGGER_SELECTOR).click
|
find(TRIGGER_SELECTOR).click
|
||||||
|
@ -26,6 +27,14 @@ module PageObjects
|
||||||
find("#{SUGGESTION_SELECTOR}__text").text
|
find("#{SUGGESTION_SELECTOR}__text").text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_highlighted_text?
|
||||||
|
page.has_css?(HIGHLIGHT_SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_no_highlighted_text?
|
||||||
|
page.has_no_css?(HIGHLIGHT_SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
def has_post_ai_helper?
|
def has_post_ai_helper?
|
||||||
page.has_css?(AI_HELPER_SELECTOR)
|
page.has_css?(AI_HELPER_SELECTOR)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue