Skip to content

Commit f148452

Browse files
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>
1 parent c6aeabb commit f148452

File tree

5 files changed

+196
-21
lines changed

5 files changed

+196
-21
lines changed

assets/javascripts/discourse/components/modal/diff-modal.gjs

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,80 @@
11
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
23
import { action } from "@ember/object";
4+
import { next } from "@ember/runloop";
5+
import { inject as service } from "@ember/service";
36
import { htmlSafe } from "@ember/template";
7+
import CookText from "discourse/components/cook-text";
48
import DButton from "discourse/components/d-button";
59
import DModal from "discourse/components/d-modal";
10+
import { ajax } from "discourse/lib/ajax";
11+
import { popupAjaxError } from "discourse/lib/ajax-error";
612
import i18n from "discourse-common/helpers/i18n";
713

814
export default class ModalDiffModal extends Component {
15+
@service currentUser;
16+
@tracked loading = false;
17+
@tracked diff;
18+
suggestion = "";
19+
20+
PROOFREAD_ID = -303;
21+
22+
constructor() {
23+
super(...arguments);
24+
this.diff = this.args.model.diff;
25+
26+
next(() => {
27+
if (this.args.model.toolbarEvent) {
28+
this.loadDiff();
29+
}
30+
});
31+
}
32+
33+
async loadDiff() {
34+
this.loading = true;
35+
36+
try {
37+
const suggestion = await ajax("/discourse-ai/ai-helper/suggest", {
38+
method: "POST",
39+
data: {
40+
mode: this.PROOFREAD_ID,
41+
text: this.selectedText,
42+
force_default_locale: true,
43+
},
44+
});
45+
46+
this.diff = suggestion.diff;
47+
this.suggestion = suggestion.suggestions[0];
48+
} catch (e) {
49+
popupAjaxError(e);
50+
} finally {
51+
this.loading = false;
52+
}
53+
}
54+
55+
get selectedText() {
56+
const selected = this.args.model.toolbarEvent.selected;
57+
58+
if (selected.value === "") {
59+
return selected.pre + selected.post;
60+
}
61+
62+
return selected.value;
63+
}
64+
965
@action
1066
triggerConfirmChanges() {
1167
this.args.closeModal();
12-
this.args.model.confirm();
68+
if (this.args.model.confirm) {
69+
this.args.model.confirm();
70+
}
71+
72+
if (this.args.model.toolbarEvent && this.suggestion) {
73+
this.args.model.toolbarEvent.replaceText(
74+
this.selectedText,
75+
this.suggestion
76+
);
77+
}
1378
}
1479

1580
@action
@@ -25,30 +90,46 @@ export default class ModalDiffModal extends Component {
2590
@closeModal={{@closeModal}}
2691
>
2792
<:body>
28-
{{#if @model.diff}}
29-
{{htmlSafe @model.diff}}
30-
{{else}}
31-
<div class="composer-ai-helper-modal__old-value">
32-
{{@model.oldValue}}
93+
{{#if this.loading}}
94+
<div class="composer-ai-helper-modal__loading">
95+
<CookText @rawText={{this.selectedText}} />
3396
</div>
97+
{{else}}
98+
{{#if this.diff}}
99+
{{htmlSafe this.diff}}
100+
{{else}}
101+
<div class="composer-ai-helper-modal__old-value">
102+
{{@model.oldValue}}
103+
</div>
34104

35-
<div class="composer-ai-helper-modal__new-value">
36-
{{@model.newValue}}
37-
</div>
105+
<div class="composer-ai-helper-modal__new-value">
106+
{{@model.newValue}}
107+
</div>
108+
{{/if}}
38109
{{/if}}
39110
</:body>
40111

41112
<:footer>
42-
<DButton
43-
class="btn-primary confirm"
44-
@action={{this.triggerConfirmChanges}}
45-
@label="discourse_ai.ai_helper.context_menu.confirm"
46-
/>
47-
<DButton
48-
class="btn-flat revert"
49-
@action={{this.triggerRevertChanges}}
50-
@label="discourse_ai.ai_helper.context_menu.revert"
51-
/>
113+
{{#if this.loading}}
114+
<DButton
115+
class="btn-primary"
116+
@label="discourse_ai.ai_helper.context_menu.loading"
117+
@disabled={{true}}
118+
/>
119+
{{else}}
120+
<DButton
121+
class="btn-primary confirm"
122+
@action={{this.triggerConfirmChanges}}
123+
@label="discourse_ai.ai_helper.context_menu.confirm"
124+
/>
125+
{{#if @model.revert}}
126+
<DButton
127+
class="btn-flat revert"
128+
@action={{this.triggerRevertChanges}}
129+
@label="discourse_ai.ai_helper.context_menu.revert"
130+
/>
131+
{{/if}}
132+
{{/if}}
52133
</:footer>
53134
</DModal>
54135
</template>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { withPluginApi } from "discourse/lib/plugin-api";
2+
import ModalDiffModal from "../discourse/components/modal/diff-modal";
3+
4+
function initializeProofread(api) {
5+
api.addComposerToolbarPopupMenuOption({
6+
action: (toolbarEvent) => {
7+
const modal = api.container.lookup("service:modal");
8+
9+
modal.show(ModalDiffModal, {
10+
model: {
11+
toolbarEvent,
12+
},
13+
});
14+
},
15+
icon: "spell-check",
16+
label: "discourse_ai.ai_helper.context_menu.proofread_prompt",
17+
shortcut: "ALT+P",
18+
condition: () => {
19+
const siteSettings = api.container.lookup("service:site-settings");
20+
const currentUser = api.getCurrentUser();
21+
22+
return (
23+
siteSettings.ai_helper_enabled && currentUser?.can_use_assistant_in_post
24+
);
25+
},
26+
});
27+
}
28+
29+
export default {
30+
name: "discourse-ai-helper",
31+
32+
initialize() {
33+
withPluginApi("1.1.0", (api) => {
34+
initializeProofread(api);
35+
});
36+
},
37+
};

assets/stylesheets/modules/ai-helper/common/ai-helper.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414
height: 200px;
1515
}
1616
}
17+
@keyframes fadeOpacity {
18+
0% {
19+
opacity: 1;
20+
}
21+
100% {
22+
opacity: 0.5;
23+
}
24+
}
25+
26+
&__loading {
27+
animation: fadeOpacity 1.5s infinite alternate;
28+
}
1729

1830
&__old-value {
1931
background-color: var(--danger-low);

config/locales/client.en.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ en:
236236
back: "Back"
237237
confirm_delete: Are you sure you want to delete this model?
238238
delete: Delete
239-
in_use_warning:
239+
in_use_warning:
240240
one: "This model is currently used by the %{settings} setting. If misconfigured, the feature won't work as expected."
241241
other: "This model is currently used by the following settings: %{settings}. If misconfigured, features won't work as expected. "
242242

@@ -294,12 +294,13 @@ en:
294294
view_changes: "View Changes"
295295
confirm: "Confirm"
296296
revert: "Revert"
297-
changes: "Changes"
297+
changes: "Suggested Edits"
298298
custom_prompt:
299299
title: "Custom Prompt"
300300
placeholder: "Enter a custom prompt..."
301301
submit: "Send Prompt"
302302
translate_prompt: "Translate to %{language}"
303+
proofread_prompt: "Proofread Text"
303304
post_options_menu:
304305
trigger: "Ask AI"
305306
title: "Ask AI"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe "AI Composer Proofreading Features", type: :system, js: true do
4+
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
5+
6+
before do
7+
assign_fake_provider_to(:ai_helper_model)
8+
SiteSetting.ai_helper_enabled = true
9+
sign_in(admin)
10+
end
11+
12+
let(:composer) { PageObjects::Components::Composer.new }
13+
14+
it "proofreads selected text using the composer toolbar" do
15+
visit "/new-topic"
16+
composer.fill_content("hello worldd !")
17+
18+
composer.select_range(6, 12)
19+
20+
DiscourseAi::Completions::Llm.with_prepared_responses(["world"]) do
21+
ai_toolbar = PageObjects::Components::SelectKit.new(".toolbar-popup-menu-options")
22+
ai_toolbar.expand
23+
ai_toolbar.select_row_by_name("Proofread Text")
24+
25+
find(".composer-ai-helper-modal .btn-primary.confirm").click
26+
expect(composer.composer_input.value).to eq("hello world !")
27+
end
28+
end
29+
30+
it "proofreads all text when nothing is selected" do
31+
visit "/new-topic"
32+
composer.fill_content("hello worrld")
33+
34+
# Simulate AI response
35+
DiscourseAi::Completions::Llm.with_prepared_responses(["hello world"]) do
36+
ai_toolbar = PageObjects::Components::SelectKit.new(".toolbar-popup-menu-options")
37+
ai_toolbar.expand
38+
ai_toolbar.select_row_by_name("Proofread Text")
39+
40+
find(".composer-ai-helper-modal .btn-primary.confirm").click
41+
expect(composer.composer_input.value).to eq("hello world")
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)