UX: Highlight AI post helper selection (#520)

This commit is contained in:
Keegan George 2024-04-04 11:35:01 -07:00 committed by GitHub
parent fc6b937df7
commit cb4d438506
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 127 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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