From 122ec652e976c195abb953c7234e5d526f42393e Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 24 May 2025 11:31:38 +1000 Subject: [PATCH 1/3] FIX: apply diffs more consistently 1. Do escaping direct in diff streamer, that way HTML tags and other unsafe chars can be displayed and fixed 2. Add safeguard to ensure streaming always stops when it was terminated elsewhere --- .../discourse/components/modal/diff-modal.gjs | 23 +++++++++++++------ .../discourse/lib/diff-streamer.gjs | 14 ++++++++--- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/assets/javascripts/discourse/components/modal/diff-modal.gjs b/assets/javascripts/discourse/components/modal/diff-modal.gjs index 0fb4f2622..6c5526687 100644 --- a/assets/javascripts/discourse/components/modal/diff-modal.gjs +++ b/assets/javascripts/discourse/components/modal/diff-modal.gjs @@ -24,8 +24,8 @@ export default class ModalDiffModal extends Component { @tracked loading = false; @tracked finalResult = ""; - @tracked selectedText = escapeExpression(this.args.model.selectedText); - @tracked diffStreamer = new DiffStreamer(this.selectedText); + @tracked escapedSelectedText = escapeExpression(this.args.model.selectedText); + @tracked diffStreamer = new DiffStreamer(this.args.model.selectedText); @tracked suggestion = ""; @tracked smoothStreamer = new SmoothStreamer( @@ -45,11 +45,16 @@ export default class ModalDiffModal extends Component { // Prevents flash by showing the // original text when the diff is empty - return this.selectedText; + return this.escapedSelectedText; } get isStreaming() { - return this.diffStreamer.isStreaming || this.smoothStreamer.isStreaming; + // diffStreamer stops "streaming" when it is finished with a chunk + return ( + this.diffStreamer.isStreaming || + !this.diffStreamer.isDone || + this.smoothStreamer.isStreaming + ); } get primaryBtnLabel() { @@ -105,7 +110,7 @@ export default class ModalDiffModal extends Component { data: { location: "composer", mode: this.args.model.mode, - text: this.selectedText, + text: this.args.model.selectedText, custom_prompt: this.args.model.customPromptValue, force_default_locale: true, client_id: this.messageBus.clientId, @@ -122,7 +127,7 @@ export default class ModalDiffModal extends Component { if (this.suggestion) { this.args.model.toolbarEvent.replaceText( - this.selectedText, + this.args.model.selectedText, this.suggestion ); } @@ -131,8 +136,12 @@ export default class ModalDiffModal extends Component { this.finalResult?.length > 0 ? this.finalResult : this.diffStreamer.suggestion; + if (this.args.model.showResultAsDiff && finalResult) { - this.args.model.toolbarEvent.replaceText(this.selectedText, finalResult); + this.args.model.toolbarEvent.replaceText( + this.args.model.selectedText, + finalResult + ); } } diff --git a/assets/javascripts/discourse/lib/diff-streamer.gjs b/assets/javascripts/discourse/lib/diff-streamer.gjs index f02a4be23..42a591f2a 100644 --- a/assets/javascripts/discourse/lib/diff-streamer.gjs +++ b/assets/javascripts/discourse/lib/diff-streamer.gjs @@ -2,8 +2,9 @@ import { tracked } from "@glimmer/tracking"; import { cancel, later } from "@ember/runloop"; import loadJSDiff from "discourse/lib/load-js-diff"; import { parseAsync } from "discourse/lib/text"; +import { escapeExpression } from "discourse/lib/utilities"; -const DEFAULT_CHAR_TYPING_DELAY = 30; +const DEFAULT_CHAR_TYPING_DELAY = 10; const STREAMING_DIFF_TRUNCATE_THRESHOLD = 0.1; const STREAMING_DIFF_TRUNCATE_BUFFER = 10; @@ -51,7 +52,7 @@ export default class DiffStreamer { const originalDiff = this.jsDiff.diffWordsWithSpace( this.selectedText, - this.suggestion + newText ); this.diff = this.#formatDiffWithTags(originalDiff, false); return; @@ -172,6 +173,10 @@ export default class DiffStreamer { } async #streamNextChar() { + if (!this.isStreaming || this.isDone) { + return; + } + if (this.currentWordIndex < this.words.length) { const currentToken = this.words[this.currentWordIndex]; @@ -252,6 +257,7 @@ export default class DiffStreamer { return `${text}`; } + // returns an HTML safe diff (escaping all internals) #formatDiffWithTags(diffArray, highlightLastWord = true) { const wordsWithType = []; const output = []; @@ -280,7 +286,8 @@ export default class DiffStreamer { } for (let i = 0; i <= lastWordIndex; i++) { - const { text, type } = wordsWithType[i]; + let { text, type } = wordsWithType[i]; + text = escapeExpression(text); if (/^\s+$/.test(text)) { output.push(text); @@ -310,6 +317,7 @@ export default class DiffStreamer { i++; } + chunkText = escapeExpression(chunkText); output.push(this.#wrapChunk(chunkText, chunkType)); } } From 2f8caa3f65f82bed7ee2e557cb0377297eb948f9 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 24 May 2025 11:41:34 +1000 Subject: [PATCH 2/3] lint --- assets/stylesheets/modules/ai-helper/common/ai-helper.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss index 5541cd90d..d17d592b0 100644 --- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss +++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss @@ -647,7 +647,7 @@ .desktop-view & { // a little extra space for extra narrow desktop view - @media screen and (width <= 675px) { + @media screen and (max-width: 675px) { span { display: none; } From 62ca8d399dde9e886fcfed20d2b63af8db8f55c1 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 24 May 2025 12:02:36 +1000 Subject: [PATCH 3/3] bug unsubscribe should unsubscribe --- assets/javascripts/discourse/components/modal/diff-modal.gjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/javascripts/discourse/components/modal/diff-modal.gjs b/assets/javascripts/discourse/components/modal/diff-modal.gjs index 6c5526687..81f938b1f 100644 --- a/assets/javascripts/discourse/components/modal/diff-modal.gjs +++ b/assets/javascripts/discourse/components/modal/diff-modal.gjs @@ -76,7 +76,7 @@ export default class ModalDiffModal extends Component { @bind unsubscribe() { const channel = "/discourse-ai/ai-helper/stream_composer_suggestion"; - this.messageBus.subscribe(channel, this.updateResult); + this.messageBus.unsubscribe(channel, this.updateResult); } @action