mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-28 18:42:16 +00:00
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 FastEditModal from "discourse/components/modal/fast-edit";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import { sanitize } from "discourse/lib/text";
|
||||||
import { clipboardCopy } from "discourse/lib/utilities";
|
import { clipboardCopy } from "discourse/lib/utilities";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
import eq from "truth-helpers/helpers/eq";
|
import eq from "truth-helpers/helpers/eq";
|
||||||
import not from "truth-helpers/helpers/not";
|
|
||||||
import AiHelperCustomPrompt from "../../components/ai-helper-custom-prompt";
|
import AiHelperCustomPrompt from "../../components/ai-helper-custom-prompt";
|
||||||
import AiHelperLoading from "../../components/ai-helper-loading";
|
import AiHelperLoading from "../../components/ai-helper-loading";
|
||||||
import { showPostAIHelper } from "../../lib/show-ai-helper";
|
import { showPostAIHelper } from "../../lib/show-ai-helper";
|
||||||
@ -42,6 +43,9 @@ export default class AIHelperOptionsMenu extends Component {
|
|||||||
@tracked showAiButtons = true;
|
@tracked showAiButtons = true;
|
||||||
@tracked originalPostHTML = null;
|
@tracked originalPostHTML = null;
|
||||||
@tracked postHighlighted = false;
|
@tracked postHighlighted = false;
|
||||||
|
@tracked streaming = false;
|
||||||
|
@tracked lastSelectedOption = null;
|
||||||
|
@tracked isSavingFootnote = false;
|
||||||
|
|
||||||
MENU_STATES = {
|
MENU_STATES = {
|
||||||
triggers: "TRIGGERS",
|
triggers: "TRIGGERS",
|
||||||
@ -170,6 +174,7 @@ export default class AIHelperOptionsMenu extends Component {
|
|||||||
|
|
||||||
@bind
|
@bind
|
||||||
_updateResult(result) {
|
_updateResult(result) {
|
||||||
|
this.streaming = !result.done;
|
||||||
this.suggestion = result.result;
|
this.suggestion = result.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +194,7 @@ export default class AIHelperOptionsMenu extends Component {
|
|||||||
@action
|
@action
|
||||||
async performAISuggestion(option) {
|
async performAISuggestion(option) {
|
||||||
this.menuState = this.MENU_STATES.loading;
|
this.menuState = this.MENU_STATES.loading;
|
||||||
|
this.lastSelectedOption = option;
|
||||||
|
|
||||||
if (option.name === "explain") {
|
if (option.name === "explain") {
|
||||||
this.menuState = this.MENU_STATES.result;
|
this.menuState = this.MENU_STATES.result;
|
||||||
@ -306,6 +312,58 @@ export default class AIHelperOptionsMenu extends Component {
|
|||||||
await this.args.outletArgs.data.hideToolbar();
|
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>
|
<template>
|
||||||
{{#if this.showMainButtons}}
|
{{#if this.showMainButtons}}
|
||||||
{{yield}}
|
{{yield}}
|
||||||
@ -356,7 +414,7 @@ export default class AIHelperOptionsMenu extends Component {
|
|||||||
<div class="ai-post-helper__suggestion__text" dir="auto">
|
<div class="ai-post-helper__suggestion__text" dir="auto">
|
||||||
<CookText @rawText={{this.suggestion}} />
|
<CookText @rawText={{this.suggestion}} />
|
||||||
</div>
|
</div>
|
||||||
<di class="ai-post-helper__suggestion__buttons">
|
<div class="ai-post-helper__suggestion__buttons">
|
||||||
<DButton
|
<DButton
|
||||||
@icon="times"
|
@icon="times"
|
||||||
@label="discourse_ai.ai_helper.post_options_menu.cancel"
|
@label="discourse_ai.ai_helper.post_options_menu.cancel"
|
||||||
@ -367,10 +425,20 @@ export default class AIHelperOptionsMenu extends Component {
|
|||||||
@icon={{this.copyButtonIcon}}
|
@icon={{this.copyButtonIcon}}
|
||||||
@label={{this.copyButtonLabel}}
|
@label={{this.copyButtonLabel}}
|
||||||
@action={{this.copySuggestion}}
|
@action={{this.copySuggestion}}
|
||||||
@disabled={{not this.suggestion}}
|
@disabled={{this.streaming}}
|
||||||
class="btn-flat ai-post-helper__suggestion__copy"
|
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}}
|
{{else}}
|
||||||
<AiHelperLoading @cancel={{this.cancelAIAction}} />
|
<AiHelperLoading @cancel={{this.cancelAIAction}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -224,6 +224,8 @@ en:
|
|||||||
copy: "Copy"
|
copy: "Copy"
|
||||||
copied: "Copied!"
|
copied: "Copied!"
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
|
insert_footnote: "Add footnote"
|
||||||
|
footnote_credits: "Explanation by AI"
|
||||||
fast_edit:
|
fast_edit:
|
||||||
suggest_button: "Suggest Edit"
|
suggest_button: "Suggest Edit"
|
||||||
thumbnail_suggestions:
|
thumbnail_suggestions:
|
||||||
|
@ -69,21 +69,47 @@ RSpec.describe "AI Post helper", type: :system, js: true do
|
|||||||
let(:mode) { CompletionPrompt::EXPLAIN }
|
let(:mode) { CompletionPrompt::EXPLAIN }
|
||||||
|
|
||||||
let(:explain_response) { <<~STRING }
|
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
|
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.
|
is being used to refer the the baked dessert food item.
|
||||||
STRING
|
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
|
it "shows an explanation of the selected text" do
|
||||||
select_post_text(post)
|
select_post_text(post)
|
||||||
post_ai_helper.click_ai_button
|
post_ai_helper.click_ai_button
|
||||||
|
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([explain_response]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([explain_response]) do
|
||||||
|
expected_value = explain_response.gsub(/"/, "").strip
|
||||||
|
|
||||||
post_ai_helper.select_helper_model(mode)
|
post_ai_helper.select_helper_model(mode)
|
||||||
wait_for { post_ai_helper.suggestion_value == explain_response }
|
Jobs.run_immediately!
|
||||||
expect(post_ai_helper.suggestion_value).to eq(explain_response)
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -19,6 +19,10 @@ module PageObjects
|
|||||||
find(TRIGGER_SELECTOR).click
|
find(TRIGGER_SELECTOR).click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def click_add_footnote
|
||||||
|
find("#{SUGGESTION_SELECTOR}__insert-footnote").click
|
||||||
|
end
|
||||||
|
|
||||||
def select_helper_model(mode)
|
def select_helper_model(mode)
|
||||||
find("#{OPTIONS_SELECTOR} .btn[data-value=\"#{mode}\"]").click
|
find("#{OPTIONS_SELECTOR} .btn[data-value=\"#{mode}\"]").click
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user