FEATURE: Insert footnote from explained result (#591)
This commit is contained in:
parent
b52d3c7d29
commit
8875830f6a
|
@ -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}}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue