Files

251 lines
6.9 KiB
Plaintext
Raw Permalink Normal View History

2023-08-24 17:49:24 -07:00
import Component from "@glimmer/component";
2024-08-26 15:43:40 +10:00
import { tracked } from "@glimmer/tracking";
2023-08-24 17:49:24 -07:00
import { action } from "@ember/object";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
2024-11-19 10:57:40 +00:00
import { service } from "@ember/service";
2023-11-03 11:30:09 +00:00
import { htmlSafe } from "@ember/template";
2025-05-30 10:35:53 -07:00
import { or } from "truth-helpers";
2024-08-26 15:43:40 +10:00
import CookText from "discourse/components/cook-text";
2023-08-24 17:49:24 -07:00
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import concatClass from "discourse/helpers/concat-class";
2024-08-26 15:43:40 +10:00
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse/lib/decorators";
import { escapeExpression } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
import DiffStreamer from "../../lib/diff-streamer";
import SmoothStreamer from "../../lib/smooth-streamer";
import AiIndicatorWave from "../ai-indicator-wave";
2023-08-24 17:49:24 -07:00
export default class ModalDiffModal extends Component {
2024-08-26 15:43:40 +10:00
@service currentUser;
@service messageBus;
2025-03-17 05:14:53 +01:00
2024-08-26 15:43:40 +10:00
@tracked loading = false;
@tracked finalResult = "";
2025-05-24 15:19:48 +10:00
@tracked escapedSelectedText = escapeExpression(this.args.model.selectedText);
@tracked diffStreamer = new DiffStreamer(this.args.model.selectedText);
@tracked suggestion = "";
@tracked
smoothStreamer = new SmoothStreamer(
() => this.suggestion,
(newValue) => (this.suggestion = newValue)
);
2024-08-26 15:43:40 +10:00
constructor() {
super(...arguments);
this.suggestChanges();
2024-08-26 15:43:40 +10:00
}
get diffResult() {
2025-05-30 10:35:53 -07:00
if (this.loading) {
return this.escapedSelectedText;
}
if (this.diffStreamer.diff?.length > 0) {
return this.diffStreamer.diff;
}
// Prevents flash by showing the
// original text when the diff is empty
2025-05-24 15:19:48 +10:00
return this.escapedSelectedText;
}
2025-05-30 10:35:53 -07:00
get smoothStreamerResult() {
if (this.loading) {
return this.escapedSelectedText;
}
return this.smoothStreamer.renderedText;
}
get isStreaming() {
2025-05-27 18:12:02 +10:00
// diffStreamer stops Streaming when it is finished with a chunk, looking at isDone is safe
// it starts off not done
2025-05-30 10:35:53 -07:00
if (this.args.model.showResultAsDiff) {
return !this.diffStreamer.isDone;
}
return this.smoothStreamer.isStreaming;
}
get primaryBtnLabel() {
return this.loading
? i18n("discourse_ai.ai_helper.context_menu.loading")
: i18n("discourse_ai.ai_helper.context_menu.confirm");
}
get primaryBtnDisabled() {
return this.loading || this.isStreaming;
}
2025-07-01 18:02:16 +10:00
set progressChannel(value) {
if (this._progressChannel) {
this.messageBus.unsubscribe(this._progressChannel, this.updateResult);
}
this._progressChannel = value;
this.subscribe();
}
subscribe() {
2025-07-01 18:02:16 +10:00
// we have 1 channel per operation so we can safely subscribe at head
this.messageBus.subscribe(this._progressChannel, this.updateResult, 0);
}
@bind
2025-05-27 18:12:02 +10:00
cleanup() {
// stop all callbacks so it does not end up streaming pointlessly
2025-06-19 07:41:18 -07:00
this.#resetState();
2025-07-01 18:02:16 +10:00
if (this._progressChannel) {
this.messageBus.unsubscribe(this._progressChannel, this.updateResult);
}
}
@action
2025-05-27 18:12:02 +10:00
updateResult(result) {
this.loading = false;
if (result.done) {
this.finalResult = result.result;
this.loading = false;
}
if (this.args.model.showResultAsDiff) {
this.diffStreamer.updateResult(result, "result");
} else {
this.smoothStreamer.updateResult(result, "result");
}
}
@action
async suggestChanges() {
this.#resetState();
2024-08-26 15:43:40 +10:00
try {
this.loading = true;
2025-07-01 18:02:16 +10:00
const result = await ajax("/discourse-ai/ai-helper/stream_suggestion", {
2024-08-26 15:43:40 +10:00
method: "POST",
data: {
location: "composer",
mode: this.args.model.mode,
2025-05-24 15:19:48 +10:00
text: this.args.model.selectedText,
custom_prompt: this.args.model.customPromptValue,
2024-08-26 15:43:40 +10:00
force_default_locale: true,
client_id: this.messageBus.clientId,
2024-08-26 15:43:40 +10:00
},
});
2025-07-01 18:02:16 +10:00
this.progressChannel = result.progress_channel;
2024-08-26 15:43:40 +10:00
} catch (e) {
popupAjaxError(e);
}
}
2023-11-03 11:30:09 +00:00
@action
triggerConfirmChanges() {
this.args.closeModal();
2024-08-26 15:43:40 +10:00
if (this.suggestion) {
2024-08-26 15:43:40 +10:00
this.args.model.toolbarEvent.replaceText(
2025-05-24 15:19:48 +10:00
this.args.model.selectedText,
2024-08-26 15:43:40 +10:00
this.suggestion
);
}
const finalResult =
this.finalResult?.length > 0
? this.finalResult
: this.diffStreamer.suggestion;
2025-05-24 15:19:48 +10:00
if (this.args.model.showResultAsDiff && finalResult) {
2025-05-24 15:19:48 +10:00
this.args.model.toolbarEvent.replaceText(
this.args.model.selectedText,
finalResult
);
}
2023-11-03 11:30:09 +00:00
}
@action
cleanupAndClose() {
this.#resetState();
this.loading = false;
this.args.closeModal();
}
#resetState() {
this.suggestion = "";
this.finalResult = "";
this.smoothStreamer.resetStreaming();
this.diffStreamer.reset();
}
2023-08-24 17:49:24 -07:00
<template>
<DModal
class="composer-ai-helper-modal"
2024-01-13 00:28:06 +01:00
@title={{i18n "discourse_ai.ai_helper.context_menu.changes"}}
@closeModal={{this.cleanupAndClose}}
2023-08-24 17:49:24 -07:00
>
<:body>
2025-07-01 18:02:16 +10:00
<div {{willDestroy this.cleanup}} class="text-preview">
2025-05-30 10:35:53 -07:00
<div
class={{concatClass
"composer-ai-helper-modal__suggestion"
"streamable-content"
(if this.isStreaming "streaming")
(if @model.showResultAsDiff "inline-diff")
(if this.diffStreamer.isThinking "thinking")
(if this.loading "composer-ai-helper-modal__loading")
}}
>
{{~#if @model.showResultAsDiff~}}
<span class="diff-inner">{{htmlSafe this.diffResult}}</span>
{{else}}
{{#if (or this.loading this.smoothStreamer.isStreaming)}}
<CookText
@rawText={{this.smoothStreamerResult}}
class="cooked"
/>
{{else}}
2025-05-30 10:35:53 -07:00
<div class="composer-ai-helper-modal__old-value">
{{~this.escapedSelectedText~}}
</div>
<div class="composer-ai-helper-modal__new-value">
<CookText
2025-05-30 10:35:53 -07:00
@rawText={{this.smoothStreamerResult}}
class="cooked"
/>
2025-05-30 10:35:53 -07:00
</div>
{{/if}}
2025-05-30 10:35:53 -07:00
{{/if}}
</div>
</div>
2023-08-24 17:49:24 -07:00
</:body>
<:footer>
<DButton
class="btn-primary confirm"
@disabled={{this.primaryBtnDisabled}}
@action={{this.triggerConfirmChanges}}
@translatedLabel={{this.primaryBtnLabel}}
>
{{#if this.loading}}
<AiIndicatorWave @loading={{this.loading}} />
{{/if}}
</DButton>
<DButton
class="btn-flat discard"
@action={{this.cleanupAndClose}}
@label="discourse_ai.ai_helper.context_menu.discard"
/>
<DButton
class="regenerate"
@icon="arrows-rotate"
@action={{this.suggestChanges}}
@label="discourse_ai.ai_helper.context_menu.regen"
/>
2023-08-24 17:49:24 -07:00
</:footer>
</DModal>
</template>
}