diff --git a/assets/javascripts/discourse/components/modal/diff-modal.gjs b/assets/javascripts/discourse/components/modal/diff-modal.gjs
index 66cba0f0..34f80953 100644
--- a/assets/javascripts/discourse/components/modal/diff-modal.gjs
+++ b/assets/javascripts/discourse/components/modal/diff-modal.gjs
@@ -1,15 +1,80 @@
import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
+import { next } from "@ember/runloop";
+import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
+import CookText from "discourse/components/cook-text";
import DButton from "discourse/components/d-button";
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";
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
triggerConfirmChanges() {
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
@@ -25,30 +90,46 @@ export default class ModalDiffModal extends Component {
@closeModal={{@closeModal}}
>
<:body>
- {{#if @model.diff}}
- {{htmlSafe @model.diff}}
+ {{#if this.loading}}
+
+
+
{{else}}
-
- {{@model.oldValue}}
-
+ {{#if this.diff}}
+ {{htmlSafe this.diff}}
+ {{else}}
+
+ {{@model.oldValue}}
+
-
- {{@model.newValue}}
-
+
+ {{@model.newValue}}
+
+ {{/if}}
{{/if}}
<:footer>
-
-
+ {{#if this.loading}}
+
+ {{else}}
+
+ {{#if @model.revert}}
+
+ {{/if}}
+ {{/if}}
diff --git a/assets/javascripts/initializers/ai-helper.js b/assets/javascripts/initializers/ai-helper.js
new file mode 100644
index 00000000..acaffa35
--- /dev/null
+++ b/assets/javascripts/initializers/ai-helper.js
@@ -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);
+ });
+ },
+};
diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
index 1af920d5..99aa408f 100644
--- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
+++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
@@ -14,6 +14,18 @@
height: 200px;
}
}
+ @keyframes fadeOpacity {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.5;
+ }
+ }
+
+ &__loading {
+ animation: fadeOpacity 1.5s infinite alternate;
+ }
&__old-value {
background-color: var(--danger-low);
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 03b5c612..faa9c1ea 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -236,7 +236,7 @@ en:
back: "Back"
confirm_delete: Are you sure you want to delete this model?
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."
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"
confirm: "Confirm"
revert: "Revert"
- changes: "Changes"
+ changes: "Suggested Edits"
custom_prompt:
title: "Custom Prompt"
placeholder: "Enter a custom prompt..."
submit: "Send Prompt"
translate_prompt: "Translate to %{language}"
+ proofread_prompt: "Proofread Text"
post_options_menu:
trigger: "Ask AI"
title: "Ask AI"
diff --git a/spec/system/ai_helper/ai_proofreading_spec.rb b/spec/system/ai_helper/ai_proofreading_spec.rb
new file mode 100644
index 00000000..3792b470
--- /dev/null
+++ b/spec/system/ai_helper/ai_proofreading_spec.rb
@@ -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