FEATURE: Insert footnote from explained result (#591)

This commit is contained in:
Keegan George 2024-05-03 11:53:17 -07:00 committed by GitHub
parent b52d3c7d29
commit 8875830f6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 109 additions and 9 deletions

View File

@ -11,10 +11,11 @@ import FastEdit from "discourse/components/fast-edit";
import FastEditModal from "discourse/components/modal/fast-edit";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { sanitize } from "discourse/lib/text";
import { clipboardCopy } from "discourse/lib/utilities";
import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
import eq from "truth-helpers/helpers/eq";
import not from "truth-helpers/helpers/not";
import AiHelperCustomPrompt from "../../components/ai-helper-custom-prompt";
import AiHelperLoading from "../../components/ai-helper-loading";
import { showPostAIHelper } from "../../lib/show-ai-helper";
@ -42,6 +43,9 @@ export default class AIHelperOptionsMenu extends Component {
@tracked showAiButtons = true;
@tracked originalPostHTML = null;
@tracked postHighlighted = false;
@tracked streaming = false;
@tracked lastSelectedOption = null;
@tracked isSavingFootnote = false;
MENU_STATES = {
triggers: "TRIGGERS",
@ -170,6 +174,7 @@ export default class AIHelperOptionsMenu extends Component {
@bind
_updateResult(result) {
this.streaming = !result.done;
this.suggestion = result.result;
}
@ -189,6 +194,7 @@ export default class AIHelperOptionsMenu extends Component {
@action
async performAISuggestion(option) {
this.menuState = this.MENU_STATES.loading;
this.lastSelectedOption = option;
if (option.name === "explain") {
this.menuState = this.MENU_STATES.result;
@ -306,6 +312,58 @@ export default class AIHelperOptionsMenu extends Component {
await this.args.outletArgs.data.hideToolbar();
}
sanitizeForFootnote(text) {
// Remove line breaks (line-breaks breaks the inline footnote display)
text = text.replace(/[\r\n]+/g, " ");
// Remove headings (headings don't work in inline footnotes)
text = text.replace(/^(#+)\s+/gm, "");
// Trim excess space
text = text.trim();
return sanitize(text);
}
@action
async insertFootnote() {
this.isSavingFootnote = true;
if (this.allowInsertFootnote) {
try {
const result = await ajax(`/posts/${this.args.outletArgs.post.id}`);
const sanitizedSuggestion = this.sanitizeForFootnote(this.suggestion);
const credits = I18n.t(
"discourse_ai.ai_helper.post_options_menu.footnote_credits"
);
const withFootnote = `${this.selectedText} ^[${sanitizedSuggestion} (${credits})]`;
const newRaw = result.raw.replace(this.selectedText, withFootnote);
await this.args.outletArgs.post.save({ raw: newRaw });
} catch (error) {
popupAjaxError(error);
} finally {
this.isSavingFootnote = false;
this.menu.close();
}
}
}
get allowInsertFootnote() {
const siteSettings = this.siteSettings;
const canEditPost = this.args.outletArgs.data.canEditPost;
if (
!siteSettings?.enable_markdown_footnotes ||
!siteSettings?.display_footnotes_inline ||
!canEditPost
) {
return false;
}
return this.lastSelectedOption?.name === "explain";
}
<template>
{{#if this.showMainButtons}}
{{yield}}
@ -356,7 +414,7 @@ export default class AIHelperOptionsMenu extends Component {
<div class="ai-post-helper__suggestion__text" dir="auto">
<CookText @rawText={{this.suggestion}} />
</div>
<di class="ai-post-helper__suggestion__buttons">
<div class="ai-post-helper__suggestion__buttons">
<DButton
@icon="times"
@label="discourse_ai.ai_helper.post_options_menu.cancel"
@ -367,10 +425,20 @@ export default class AIHelperOptionsMenu extends Component {
@icon={{this.copyButtonIcon}}
@label={{this.copyButtonLabel}}
@action={{this.copySuggestion}}
@disabled={{not this.suggestion}}
@disabled={{this.streaming}}
class="btn-flat ai-post-helper__suggestion__copy"
/>
</di>
{{#if this.allowInsertFootnote}}
<DButton
@icon="asterisk"
@label="discourse_ai.ai_helper.post_options_menu.insert_footnote"
@action={{this.insertFootnote}}
@isLoading={{this.isSavingFootnote}}
@disabled={{this.streaming}}
class="btn-flat ai-post-helper__suggestion__insert-footnote"
/>
{{/if}}
</div>
{{else}}
<AiHelperLoading @cancel={{this.cancelAIAction}} />
{{/if}}

View File

@ -224,6 +224,8 @@ en:
copy: "Copy"
copied: "Copied!"
cancel: "Cancel"
insert_footnote: "Add footnote"
footnote_credits: "Explanation by AI"
fast_edit:
suggest_button: "Suggest Edit"
thumbnail_suggestions:

View File

@ -69,21 +69,47 @@ RSpec.describe "AI Post helper", type: :system, js: true do
let(:mode) { CompletionPrompt::EXPLAIN }
let(:explain_response) { <<~STRING }
In this context, \"pie\" refers to a baked dessert typically consisting of a pastry crust and filling.
In this context, pie refers to a baked dessert typically consisting of a pastry crust and filling.
The person states they enjoy eating pie, considering it a good dessert. They note that some people wastefully
throw pie at others, but the person themselves chooses to eat the pie rather than throwing it. Overall, \"pie\"
throw pie at others, but the person themselves chooses to eat the pie rather than throwing it. Overall, pie
is being used to refer the the baked dessert food item.
STRING
skip "TODO: Fix explain option stuck in loading in test" do
skip "TODO: Streaming causing timing issue in test" do
it "shows an explanation of the selected text" do
select_post_text(post)
post_ai_helper.click_ai_button
DiscourseAi::Completions::Llm.with_prepared_responses([explain_response]) do
expected_value = explain_response.gsub(/"/, "").strip
post_ai_helper.select_helper_model(mode)
wait_for { post_ai_helper.suggestion_value == explain_response }
expect(post_ai_helper.suggestion_value).to eq(explain_response)
Jobs.run_immediately!
wait_for(timeout: 10) do
post_ai_helper.suggestion_value.gsub(/"/, "").strip == expected_value
end
expect(post_ai_helper.suggestion_value.gsub(/"/, "").strip).to eq(expected_value)
end
end
it "adds explained text as footnote to post" do
select_post_text(post)
post_ai_helper.click_ai_button
DiscourseAi::Completions::Llm.with_prepared_responses([explain_response]) do
expected_value = explain_response.gsub(/"/, "").strip
post_ai_helper.select_helper_model(mode)
Jobs.run_immediately!
wait_for(timeout: 10) do
post_ai_helper.suggestion_value.gsub(/"/, "").strip == expected_value
end
post_ai_helper.click_add_footnote
expect(page.has_css?(".expand-footnote")).to eq(true)
end
end
end

View File

@ -19,6 +19,10 @@ module PageObjects
find(TRIGGER_SELECTOR).click
end
def click_add_footnote
find("#{SUGGESTION_SELECTOR}__insert-footnote").click
end
def select_helper_model(mode)
find("#{OPTIONS_SELECTOR} .btn[data-value=\"#{mode}\"]").click
end