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 showFastEdit = false;
|
||||
@tracked showAiButtons = true;
|
||||
@tracked originalPostHTML = null;
|
||||
@tracked postHighlighted = false;
|
||||
|
||||
MENU_STATES = {
|
||||
triggers: "TRIGGERS",
|
||||
|
@ -50,8 +52,99 @@ export default class AIHelperOptionsMenu extends Component {
|
|||
|
||||
@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
|
||||
async showAIHelperOptions() {
|
||||
this.highlightSelectedText();
|
||||
this.showMainButtons = false;
|
||||
this.menuState = this.MENU_STATES.options;
|
||||
}
|
||||
|
@ -75,6 +168,19 @@ export default class AIHelperOptionsMenu extends Component {
|
|||
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
|
||||
async performAISuggestion(option) {
|
||||
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)
|
||||
.dot-falling {
|
||||
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
|
||||
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
|
||||
let(:mode) { CompletionPrompt::EXPLAIN }
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ module PageObjects
|
|||
OPTIONS_SELECTOR = "#{AI_HELPER_SELECTOR}__options"
|
||||
LOADING_SELECTOR = ".ai-helper-context-menu__loading"
|
||||
SUGGESTION_SELECTOR = "#{AI_HELPER_SELECTOR}__suggestion"
|
||||
HIGHLIGHT_SELECTOR = ".ai-helper-highlighted-selection"
|
||||
|
||||
def click_ai_button
|
||||
find(TRIGGER_SELECTOR).click
|
||||
|
@ -26,6 +27,14 @@ module PageObjects
|
|||
find("#{SUGGESTION_SELECTOR}__text").text
|
||||
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?
|
||||
page.has_css?(AI_HELPER_SELECTOR)
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue