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 all 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
26 changes: 22 additions & 4 deletions assets/javascripts/discourse/components/ai-post-helper-menu.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CookText from "discourse/components/cook-text";
import DButton from "discourse/components/d-button";
import FastEdit from "discourse/components/fast-edit";
import FastEditModal from "discourse/components/modal/fast-edit";
import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse/lib/decorators";
Expand All @@ -20,6 +21,7 @@ import { i18n } from "discourse-i18n";
import eq from "truth-helpers/helpers/eq";
import AiHelperLoading from "../components/ai-helper-loading";
import AiHelperOptionsList from "../components/ai-helper-options-list";
import SmoothStreamer from "../lib/smooth-streamer";

export default class AiPostHelperMenu extends Component {
@service messageBus;
Expand All @@ -43,6 +45,12 @@ export default class AiPostHelperMenu extends Component {
@tracked isSavingFootnote = false;
@tracked supportsAddFootnote = this.args.data.supportsFastEdit;

@tracked
smoothStreamer = new SmoothStreamer(
() => this.suggestion,
(newValue) => (this.suggestion = newValue)
);

MENU_STATES = {
options: "OPTIONS",
loading: "LOADING",
Expand Down Expand Up @@ -172,9 +180,9 @@ export default class AiPostHelperMenu extends Component {
}

@bind
_updateResult(result) {
async _updateResult(result) {
this.streaming = !result.done;
this.suggestion = result.result;
await this.smoothStreamer.updateResult(result, "result");
}

@action
Expand Down Expand Up @@ -350,8 +358,18 @@ export default class AiPostHelperMenu extends Component {
{{willDestroy this.unsubscribe}}
>
{{#if this.suggestion}}
<div class="ai-post-helper__suggestion__text" dir="auto">
<CookText @rawText={{this.suggestion}} />
<div
class={{concatClass
(if this.smoothStreamer.isStreaming "streaming")
"streamable-content"
"ai-post-helper__suggestion__text"
}}
dir="auto"
>
<CookText
@rawText={{this.smoothStreamer.renderedText}}
class="cooked"
/>
</div>
<div class="ai-post-helper__suggestion__buttons">
<DButton
Expand Down
70 changes: 11 additions & 59 deletions assets/javascripts/discourse/components/ai-search-discoveries.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax";
import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import SmoothStreamer from "../lib/smooth-streamer";
import AiBlinkingAnimation from "./ai-blinking-animation";

const DISCOVERY_TIMEOUT_MS = 10000;
const STREAMED_TEXT_SPEED = 23;

export default class AiSearchDiscoveries extends Component {
@service search;
Expand All @@ -27,9 +27,11 @@ export default class AiSearchDiscoveries extends Component {
@tracked hideDiscoveries = false;
@tracked fullDiscoveryToggled = false;
@tracked discoveryPreviewLength = this.args.discoveryPreviewLength || 150;

@tracked isStreaming = false;
@tracked streamedText = "";
@tracked
smoothStreamer = new SmoothStreamer(
() => this.discobotDiscoveries.discovery,
(newValue) => (this.discobotDiscoveries.discovery = newValue)
);

discoveryTimeout = null;
typingTimer = null;
Expand All @@ -53,36 +55,6 @@ export default class AiSearchDiscoveries extends Component {
);
}

typeCharacter() {
if (this.streamedTextLength < this.discobotDiscoveries.discovery.length) {
this.streamedText += this.discobotDiscoveries.discovery.charAt(
this.streamedTextLength
);
this.streamedTextLength++;

this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED);
} else {
this.typingTimer = null;
}
}

onTextUpdate() {
this.cancelTypingTimer();
this.typeCharacter();
}

cancelTypingTimer() {
if (this.typingTimer) {
cancel(this.typingTimer);
}
}

resetStreaming() {
this.cancelTypingTimer();
this.streamedText = "";
this.streamedTextLength = 0;
}

@bind
async _updateDiscovery(update) {
if (this.query === update.query) {
Expand All @@ -94,23 +66,9 @@ export default class AiSearchDiscoveries extends Component {
this.discobotDiscoveries.discovery = "";
}

const newText = update.ai_discover_reply;
this.discobotDiscoveries.modelUsed = update.model_used;
this.loadingDiscoveries = false;

// Handling short replies.
if (update.done) {
this.discobotDiscoveries.discovery = newText;
this.streamedText = newText;
this.isStreaming = false;

// Clear pending animations
this.cancelTypingTimer();
} else if (newText.length > this.discobotDiscoveries.discovery.length) {
this.discobotDiscoveries.discovery = newText;
this.isStreaming = true;
await this.onTextUpdate();
}
this.smoothStreamer.updateResult(update, "ai_discover_reply");
}
}

Expand Down Expand Up @@ -153,16 +111,10 @@ export default class AiSearchDiscoveries extends Component {
get canShowExpandtoggle() {
return (
!this.loadingDiscoveries &&
this.renderedDiscovery.length > this.discoveryPreviewLength
this.smoothStreamer.renderedText.length > this.discoveryPreviewLength
);
}

get renderedDiscovery() {
return this.isStreaming
? this.streamedText
: this.discobotDiscoveries.discovery;
}

get renderPreviewOnly() {
return !this.fullDiscoveryToggled && this.canShowExpandtoggle;
}
Expand All @@ -173,7 +125,7 @@ export default class AiSearchDiscoveries extends Component {
this.hideDiscoveries = false;
return;
} else {
this.resetStreaming();
this.smoothStreamer.resetStreaming();
this.discobotDiscoveries.resetDiscovery();
}

Expand Down Expand Up @@ -225,12 +177,12 @@ export default class AiSearchDiscoveries extends Component {
class={{concatClass
"ai-search-discoveries__discovery"
(if this.renderPreviewOnly "preview")
(if this.isStreaming "streaming")
(if this.smoothStreamer.isStreaming "streaming")
"streamable-content"
}}
>
<div class="cooked">
<CookText @rawText={{this.renderedDiscovery}} />
<CookText @rawText={{this.smoothStreamer.renderedText}} />
</div>
</article>

Expand Down
56 changes: 11 additions & 45 deletions assets/javascripts/discourse/components/modal/ai-summary-modal.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { cancel, later } from "@ember/runloop";
import { service } from "@ember/service";
import { not } from "truth-helpers";
import CookText from "discourse/components/cook-text";
Expand All @@ -20,8 +19,7 @@ import { shortDateNoYear } from "discourse/lib/formatter";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
import AiSummarySkeleton from "../../components/ai-summary-skeleton";

const STREAMED_TEXT_SPEED = 15;
import SmoothStreamer from "../../lib/smooth-streamer";

export default class AiSummaryModal extends Component {
@service siteSettings;
Expand All @@ -37,11 +35,12 @@ export default class AiSummaryModal extends Component {
@tracked outdated = false;
@tracked canRegenerate = false;
@tracked loading = false;
@tracked isStreaming = false;
@tracked streamedText = "";
@tracked currentIndex = 0;
typingTimer = null;
streamedTextLength = 0;
@tracked
smoothStreamer = new SmoothStreamer(
() => this.text,
(newValue) => (this.text = newValue)
);

get outdatedSummaryWarningText() {
let outdatedText = i18n("summary.outdated");
Expand All @@ -57,7 +56,7 @@ export default class AiSummaryModal extends Component {
}

resetSummary() {
this.streamedText = "";
this.smoothStreamer.resetStreaming();
this.currentIndex = 0;
this.text = "";
this.summarizedOn = null;
Expand Down Expand Up @@ -142,58 +141,25 @@ export default class AiSummaryModal extends Component {
});
}

typeCharacter() {
if (this.streamedTextLength < this.text.length) {
this.streamedText += this.text.charAt(this.streamedTextLength);
this.streamedTextLength++;

this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED);
} else {
this.typingTimer = null;
}
}

onTextUpdate() {
// Reset only if there’s a new summary to process
if (this.typingTimer) {
cancel(this.typingTimer);
}

this.typeCharacter();
}

@bind
async _updateSummary(update) {
const topicSummary = {
done: update.done,
raw: update.ai_topic_summary?.summarized_text,
...update.ai_topic_summary,
};
const newText = topicSummary.raw || "";

this.loading = false;
this.smoothStreamer.updateResult(topicSummary, "raw");

if (update.done) {
this.text = newText;
this.streamedText = newText;
this.displayedTextLength = newText.length;
this.isStreaming = false;
this.summarizedOn = shortDateNoYear(
moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z")
);
this.summarizedBy = topicSummary.algorithm;
this.newPostsSinceSummary = topicSummary.new_posts_since_summary;
this.outdated = topicSummary.outdated;
this.canRegenerate = topicSummary.outdated && topicSummary.can_regenerate;

// Clear pending animations
if (this.typingTimer) {
cancel(this.typingTimer);
this.typingTimer = null;
}
} else if (newText.length > this.text.length) {
this.text = newText;
this.isStreaming = true;
this.onTextUpdate();
}
}

Expand Down Expand Up @@ -226,14 +192,14 @@ export default class AiSummaryModal extends Component {
class={{concatClass
"ai-summary-box"
"streamable-content"
(if this.isStreaming "streaming")
(if this.smoothStreamer.isStreaming "streaming")
}}
>
{{#if this.loading}}
<AiSummarySkeleton />
{{else}}
<div class="generated-summary cooked">
<CookText @rawText={{this.streamedText}} />
<CookText @rawText={{this.smoothStreamer.renderedText}} />
</div>
{{/if}}
</article>
Expand Down
Loading