FEATURE: single click proofreading (#769)
Previously there was too much work proofreading text, new implementation provides a single shortcut and easy way of proofreading text. Co-authored-by: Martin Brennan <martin@discourse.org>
This commit is contained in:
parent
c6aeabbfc0
commit
f148452f4c
|
@ -1,15 +1,80 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { next } from "@ember/runloop";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import CookText from "discourse/components/cook-text";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import DModal from "discourse/components/d-modal";
|
import DModal from "discourse/components/d-modal";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
export default class ModalDiffModal extends Component {
|
export default class ModalDiffModal extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
@tracked loading = false;
|
||||||
|
@tracked diff;
|
||||||
|
suggestion = "";
|
||||||
|
|
||||||
|
PROOFREAD_ID = -303;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.diff = this.args.model.diff;
|
||||||
|
|
||||||
|
next(() => {
|
||||||
|
if (this.args.model.toolbarEvent) {
|
||||||
|
this.loadDiff();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDiff() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const suggestion = await ajax("/discourse-ai/ai-helper/suggest", {
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
mode: this.PROOFREAD_ID,
|
||||||
|
text: this.selectedText,
|
||||||
|
force_default_locale: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.diff = suggestion.diff;
|
||||||
|
this.suggestion = suggestion.suggestions[0];
|
||||||
|
} catch (e) {
|
||||||
|
popupAjaxError(e);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedText() {
|
||||||
|
const selected = this.args.model.toolbarEvent.selected;
|
||||||
|
|
||||||
|
if (selected.value === "") {
|
||||||
|
return selected.pre + selected.post;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected.value;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
triggerConfirmChanges() {
|
triggerConfirmChanges() {
|
||||||
this.args.closeModal();
|
this.args.closeModal();
|
||||||
this.args.model.confirm();
|
if (this.args.model.confirm) {
|
||||||
|
this.args.model.confirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.args.model.toolbarEvent && this.suggestion) {
|
||||||
|
this.args.model.toolbarEvent.replaceText(
|
||||||
|
this.selectedText,
|
||||||
|
this.suggestion
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -25,30 +90,46 @@ export default class ModalDiffModal extends Component {
|
||||||
@closeModal={{@closeModal}}
|
@closeModal={{@closeModal}}
|
||||||
>
|
>
|
||||||
<:body>
|
<:body>
|
||||||
{{#if @model.diff}}
|
{{#if this.loading}}
|
||||||
{{htmlSafe @model.diff}}
|
<div class="composer-ai-helper-modal__loading">
|
||||||
|
<CookText @rawText={{this.selectedText}} />
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="composer-ai-helper-modal__old-value">
|
{{#if this.diff}}
|
||||||
{{@model.oldValue}}
|
{{htmlSafe this.diff}}
|
||||||
</div>
|
{{else}}
|
||||||
|
<div class="composer-ai-helper-modal__old-value">
|
||||||
|
{{@model.oldValue}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="composer-ai-helper-modal__new-value">
|
<div class="composer-ai-helper-modal__new-value">
|
||||||
{{@model.newValue}}
|
{{@model.newValue}}
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</:body>
|
</:body>
|
||||||
|
|
||||||
<:footer>
|
<:footer>
|
||||||
<DButton
|
{{#if this.loading}}
|
||||||
class="btn-primary confirm"
|
<DButton
|
||||||
@action={{this.triggerConfirmChanges}}
|
class="btn-primary"
|
||||||
@label="discourse_ai.ai_helper.context_menu.confirm"
|
@label="discourse_ai.ai_helper.context_menu.loading"
|
||||||
/>
|
@disabled={{true}}
|
||||||
<DButton
|
/>
|
||||||
class="btn-flat revert"
|
{{else}}
|
||||||
@action={{this.triggerRevertChanges}}
|
<DButton
|
||||||
@label="discourse_ai.ai_helper.context_menu.revert"
|
class="btn-primary confirm"
|
||||||
/>
|
@action={{this.triggerConfirmChanges}}
|
||||||
|
@label="discourse_ai.ai_helper.context_menu.confirm"
|
||||||
|
/>
|
||||||
|
{{#if @model.revert}}
|
||||||
|
<DButton
|
||||||
|
class="btn-flat revert"
|
||||||
|
@action={{this.triggerRevertChanges}}
|
||||||
|
@label="discourse_ai.ai_helper.context_menu.revert"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
</:footer>
|
</:footer>
|
||||||
</DModal>
|
</DModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import ModalDiffModal from "../discourse/components/modal/diff-modal";
|
||||||
|
|
||||||
|
function initializeProofread(api) {
|
||||||
|
api.addComposerToolbarPopupMenuOption({
|
||||||
|
action: (toolbarEvent) => {
|
||||||
|
const modal = api.container.lookup("service:modal");
|
||||||
|
|
||||||
|
modal.show(ModalDiffModal, {
|
||||||
|
model: {
|
||||||
|
toolbarEvent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: "spell-check",
|
||||||
|
label: "discourse_ai.ai_helper.context_menu.proofread_prompt",
|
||||||
|
shortcut: "ALT+P",
|
||||||
|
condition: () => {
|
||||||
|
const siteSettings = api.container.lookup("service:site-settings");
|
||||||
|
const currentUser = api.getCurrentUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
siteSettings.ai_helper_enabled && currentUser?.can_use_assistant_in_post
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "discourse-ai-helper",
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
withPluginApi("1.1.0", (api) => {
|
||||||
|
initializeProofread(api);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -14,6 +14,18 @@
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@keyframes fadeOpacity {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading {
|
||||||
|
animation: fadeOpacity 1.5s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
&__old-value {
|
&__old-value {
|
||||||
background-color: var(--danger-low);
|
background-color: var(--danger-low);
|
||||||
|
|
|
@ -236,7 +236,7 @@ en:
|
||||||
back: "Back"
|
back: "Back"
|
||||||
confirm_delete: Are you sure you want to delete this model?
|
confirm_delete: Are you sure you want to delete this model?
|
||||||
delete: Delete
|
delete: Delete
|
||||||
in_use_warning:
|
in_use_warning:
|
||||||
one: "This model is currently used by the %{settings} setting. If misconfigured, the feature won't work as expected."
|
one: "This model is currently used by the %{settings} setting. If misconfigured, the feature won't work as expected."
|
||||||
other: "This model is currently used by the following settings: %{settings}. If misconfigured, features won't work as expected. "
|
other: "This model is currently used by the following settings: %{settings}. If misconfigured, features won't work as expected. "
|
||||||
|
|
||||||
|
@ -294,12 +294,13 @@ en:
|
||||||
view_changes: "View Changes"
|
view_changes: "View Changes"
|
||||||
confirm: "Confirm"
|
confirm: "Confirm"
|
||||||
revert: "Revert"
|
revert: "Revert"
|
||||||
changes: "Changes"
|
changes: "Suggested Edits"
|
||||||
custom_prompt:
|
custom_prompt:
|
||||||
title: "Custom Prompt"
|
title: "Custom Prompt"
|
||||||
placeholder: "Enter a custom prompt..."
|
placeholder: "Enter a custom prompt..."
|
||||||
submit: "Send Prompt"
|
submit: "Send Prompt"
|
||||||
translate_prompt: "Translate to %{language}"
|
translate_prompt: "Translate to %{language}"
|
||||||
|
proofread_prompt: "Proofread Text"
|
||||||
post_options_menu:
|
post_options_menu:
|
||||||
trigger: "Ask AI"
|
trigger: "Ask AI"
|
||||||
title: "Ask AI"
|
title: "Ask AI"
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "AI Composer Proofreading Features", type: :system, js: true do
|
||||||
|
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
assign_fake_provider_to(:ai_helper_model)
|
||||||
|
SiteSetting.ai_helper_enabled = true
|
||||||
|
sign_in(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:composer) { PageObjects::Components::Composer.new }
|
||||||
|
|
||||||
|
it "proofreads selected text using the composer toolbar" do
|
||||||
|
visit "/new-topic"
|
||||||
|
composer.fill_content("hello worldd !")
|
||||||
|
|
||||||
|
composer.select_range(6, 12)
|
||||||
|
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(["world"]) do
|
||||||
|
ai_toolbar = PageObjects::Components::SelectKit.new(".toolbar-popup-menu-options")
|
||||||
|
ai_toolbar.expand
|
||||||
|
ai_toolbar.select_row_by_name("Proofread Text")
|
||||||
|
|
||||||
|
find(".composer-ai-helper-modal .btn-primary.confirm").click
|
||||||
|
expect(composer.composer_input.value).to eq("hello world !")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "proofreads all text when nothing is selected" do
|
||||||
|
visit "/new-topic"
|
||||||
|
composer.fill_content("hello worrld")
|
||||||
|
|
||||||
|
# Simulate AI response
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(["hello world"]) do
|
||||||
|
ai_toolbar = PageObjects::Components::SelectKit.new(".toolbar-popup-menu-options")
|
||||||
|
ai_toolbar.expand
|
||||||
|
ai_toolbar.select_row_by_name("Proofread Text")
|
||||||
|
|
||||||
|
find(".composer-ai-helper-modal .btn-primary.confirm").click
|
||||||
|
expect(composer.composer_input.value).to eq("hello world")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue