Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 87 additions & 21 deletions assets/javascripts/discourse/components/modal/diff-modal.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import SmoothStreamer from "../../lib/smooth-streamer";
import AiIndicatorWave from "../ai-indicator-wave";
import { cancel, later } from "@ember/runloop";

const WORD_TYPING_DELAY = 200;

export default class ModalDiffModal extends Component {
@service currentUser;
Expand All @@ -23,11 +26,18 @@ export default class ModalDiffModal extends Component {
@tracked loading = false;
@tracked diff;
@tracked suggestion = "";
@tracked
smoothStreamer = new SmoothStreamer(
() => this.suggestion,
(newValue) => (this.suggestion = newValue)
);
@tracked isStreaming = false;
@tracked lastResultText = "";
// @tracked
// smoothStreamer = new SmoothStreamer(
// () => this.suggestion,
// (newValue) => (this.suggestion = newValue)
// );
@tracked finalDiff = "";
@tracked words = [];
originalWords = [];
typingTimer = null;
currentWordIndex = 0;

constructor() {
super(...arguments);
Expand All @@ -46,35 +56,89 @@ export default class ModalDiffModal extends Component {
this.messageBus.subscribe(channel, this.updateResult);
}

compareText(oldText = "", newText = "") {
const oldWords = oldText.trim().split(/\s+/);
const newWords = newText.trim().split(/\s+/);

const diff = [];
let i = 0;

while (i < newWords.length) {
const oldWord = oldWords[i];
const newWord = newWords[i];

let wordHTML;
if (oldWord === undefined) {
wordHTML = `<ins>${newWord}</ins>`;
} else if (oldWord !== newWord) {
wordHTML = `<del>${oldWord}</del> <ins>${newWord}</ins>`;
} else {
wordHTML = newWord;
}

if (i === newWords.length - 1) {
wordHTML = `<mark class="highlight">${wordHTML}</mark>`;
}

diff.push(wordHTML);
i++;
}

return diff.join(" ");
}

@action
async updateResult(result) {
if (result) {
this.loading = false;
this.loading = false;

const newText = result.result;
const diffText = newText.slice(this.lastResultText.length).trim();
const newWords = diffText.split(/\s+/).filter(Boolean);

if (newWords.length > 0) {
this.words.push(...newWords);
if (!this.typingTimer) {
this.streamNextWord();
}
}
await this.smoothStreamer.updateResult(result, "result");

if (result.done) {
this.diff = result.diff;
this.finalDiff = result.diff;
}

this.lastResultText = newText;

this.isStreaming = !result.done;
}

streamNextWord() {
if (this.currentWordIndex === this.words.length) {
this.diff = this.finalDiff;
}

const mdTablePromptId = this.currentUser?.ai_helper_prompts.find(
(prompt) => prompt.name === "markdown_table"
).id;
if (this.currentWordIndex < this.words.length) {
this.suggestion += this.words[this.currentWordIndex] + " ";
this.diff = this.compareText(
this.args.model.selectedText,
this.suggestion
);

// Markdown table prompt looks better with
// before/after results than diff
// despite having `type: diff`
if (this.args.model.mode === mdTablePromptId) {
this.diff = null;
this.currentWordIndex++;
this.typingTimer = later(this, this.streamNextWord, WORD_TYPING_DELAY);
} else {
this.typingTimer = null;
}
}

@action
async suggestChanges() {
this.smoothStreamer.resetStreaming();
// this.smoothStreamer.resetStreaming();
this.diff = null;
this.suggestion = "";
this.loading = true;
this.lastResultText = "";
this.words = [];
this.currentWordIndex = 0;

try {
return await ajax("/discourse-ai/ai-helper/stream_suggestion", {
Expand Down Expand Up @@ -123,10 +187,12 @@ export default class ModalDiffModal extends Component {
class={{concatClass
"composer-ai-helper-modal__suggestion"
"streamable-content"
(if this.smoothStreamer.isStreaming "streaming" "")
}}
>
{{#if this.smoothStreamer.isStreaming}}
<CookText @rawText={{this.diff}} class="cooked" />
{{!-- <div class="composer-ai-helper-modal__old-value">
{{@model.selectedText}}
{{!-- {{#if this.smoothStreamer.isStreaming}}
<CookText
@rawText={{this.smoothStreamer.renderedText}}
class="cooked"
Expand All @@ -145,7 +211,7 @@ export default class ModalDiffModal extends Component {
/>
</div>
{{/if}}
{{/if}}
{{/if}} --}}
</div>
{{/if}}
</div>
Expand Down
23 changes: 23 additions & 0 deletions assets/stylesheets/modules/ai-helper/common/ai-helper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,34 @@
text-decoration: line-through;
}

mark {
background-color: var(--highlight-low);
}

.preview-area {
height: 200px;
}
}

// TODO: cleanup and scope to inline-diff class
.composer-ai-helper-modal__suggestion .cooked {
ins,
del {
text-decoration: none;
}

mark {
background-color: var(--highlight-low) !important;
border-bottom: 2px solid var(--highlight-high) !important;

ins,
del {
background: transparent;
text-decoration: none;
}
}
}

@keyframes fadeOpacity {
0% {
opacity: 1;
Expand Down
Loading