2024-07-02 11:51:59 -04:00
|
|
|
|
import Component from "@glimmer/component";
|
|
|
|
|
import { tracked } from "@glimmer/tracking";
|
|
|
|
|
import { array } from "@ember/helper";
|
|
|
|
|
import { action } from "@ember/object";
|
|
|
|
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
2024-08-13 07:47:47 -04:00
|
|
|
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
2024-07-02 11:51:59 -04:00
|
|
|
|
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
2024-11-07 11:08:32 -05:00
|
|
|
|
import { cancel, later } from "@ember/runloop";
|
2024-07-02 11:51:59 -04:00
|
|
|
|
import { service } from "@ember/service";
|
2024-11-07 11:08:32 -05:00
|
|
|
|
import CookText from "discourse/components/cook-text";
|
2024-07-02 11:51:59 -04:00
|
|
|
|
import DButton from "discourse/components/d-button";
|
2024-10-21 12:15:25 -04:00
|
|
|
|
import concatClass from "discourse/helpers/concat-class";
|
2024-07-02 11:51:59 -04:00
|
|
|
|
import { ajax } from "discourse/lib/ajax";
|
|
|
|
|
import { shortDateNoYear } from "discourse/lib/formatter";
|
|
|
|
|
import dIcon from "discourse-common/helpers/d-icon";
|
|
|
|
|
import i18n from "discourse-common/helpers/i18n";
|
|
|
|
|
import { bind } from "discourse-common/utils/decorators";
|
|
|
|
|
import I18n from "discourse-i18n";
|
2024-07-25 09:47:18 -04:00
|
|
|
|
import DMenu from "float-kit/components/d-menu";
|
2024-07-02 11:51:59 -04:00
|
|
|
|
import DTooltip from "float-kit/components/d-tooltip";
|
|
|
|
|
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
|
2024-11-07 11:08:32 -05:00
|
|
|
|
|
|
|
|
|
const STREAMED_TEXT_SPEED = 15;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
|
|
|
|
|
export default class AiSummaryBox extends Component {
|
|
|
|
|
@service siteSettings;
|
|
|
|
|
@service messageBus;
|
|
|
|
|
@service currentUser;
|
2024-08-05 11:39:08 -04:00
|
|
|
|
@service site;
|
2024-07-25 09:47:18 -04:00
|
|
|
|
|
2024-07-02 11:51:59 -04:00
|
|
|
|
@tracked text = "";
|
|
|
|
|
@tracked summarizedOn = null;
|
|
|
|
|
@tracked summarizedBy = null;
|
|
|
|
|
@tracked newPostsSinceSummary = null;
|
|
|
|
|
@tracked outdated = false;
|
|
|
|
|
@tracked canRegenerate = false;
|
|
|
|
|
@tracked loading = false;
|
2024-10-21 12:15:25 -04:00
|
|
|
|
@tracked isStreaming = false;
|
2024-11-07 11:08:32 -05:00
|
|
|
|
@tracked streamedText = "";
|
|
|
|
|
@tracked currentIndex = 0;
|
|
|
|
|
typingTimer = null;
|
|
|
|
|
streamedTextLength = 0;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
|
|
|
|
|
get outdatedSummaryWarningText() {
|
|
|
|
|
let outdatedText = I18n.t("summary.outdated");
|
|
|
|
|
|
|
|
|
|
if (!this.topRepliesSummaryEnabled && this.newPostsSinceSummary > 0) {
|
|
|
|
|
outdatedText += " ";
|
|
|
|
|
outdatedText += I18n.t("summary.outdated_posts", {
|
|
|
|
|
count: this.newPostsSinceSummary,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return outdatedText;
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-13 07:47:47 -04:00
|
|
|
|
resetSummary() {
|
2024-11-07 11:08:32 -05:00
|
|
|
|
this.streamedText = "";
|
|
|
|
|
this.currentIndex = 0;
|
2024-08-13 07:47:47 -04:00
|
|
|
|
this.text = "";
|
|
|
|
|
this.summarizedOn = null;
|
|
|
|
|
this.summarizedBy = null;
|
|
|
|
|
this.newPostsSinceSummary = null;
|
|
|
|
|
this.outdated = false;
|
|
|
|
|
this.canRegenerate = false;
|
|
|
|
|
this.loading = false;
|
|
|
|
|
this._channel = null;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-02 11:51:59 -04:00
|
|
|
|
get topRepliesSummaryEnabled() {
|
|
|
|
|
return this.args.outletArgs.postStream.summary;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-25 09:47:18 -04:00
|
|
|
|
get topicId() {
|
|
|
|
|
return this.args.outletArgs.topic.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get baseSummarizationURL() {
|
|
|
|
|
return `/discourse-ai/summarization/t/${this.topicId}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bind
|
2024-08-20 09:57:23 -04:00
|
|
|
|
subscribe(unsubscribe, [topicId]) {
|
|
|
|
|
const sameTopicId = this.args.outletArgs.topic.id === topicId;
|
|
|
|
|
|
|
|
|
|
if (unsubscribe && this._channel && !sameTopicId) {
|
2024-08-13 07:47:47 -04:00
|
|
|
|
this.unsubscribe();
|
|
|
|
|
}
|
2024-07-25 09:47:18 -04:00
|
|
|
|
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
|
2024-08-13 07:47:47 -04:00
|
|
|
|
this._channel = channel;
|
2024-11-07 11:08:32 -05:00
|
|
|
|
this.messageBus.subscribe(channel, this._updateSummary);
|
2024-07-25 09:47:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bind
|
|
|
|
|
unsubscribe() {
|
|
|
|
|
this.messageBus.unsubscribe(
|
|
|
|
|
"/discourse-ai/summaries/topic/*",
|
|
|
|
|
this._updateSummary
|
|
|
|
|
);
|
2024-08-13 07:47:47 -04:00
|
|
|
|
this.resetSummary();
|
2024-07-02 11:51:59 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@action
|
|
|
|
|
generateSummary() {
|
2024-07-25 09:47:18 -04:00
|
|
|
|
let fetchURL = this.baseSummarizationURL;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
|
2024-07-25 09:47:18 -04:00
|
|
|
|
if (this.currentUser) {
|
|
|
|
|
fetchURL += `?stream=true`;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-25 09:47:18 -04:00
|
|
|
|
return this._requestSummary(fetchURL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@action
|
|
|
|
|
regenerateSummary() {
|
|
|
|
|
let fetchURL = this.baseSummarizationURL;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
|
|
|
|
|
if (this.currentUser) {
|
2024-07-25 09:47:18 -04:00
|
|
|
|
fetchURL += `?stream=true`;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
|
|
|
|
|
if (this.canRegenerate) {
|
|
|
|
|
fetchURL += "&skip_age_check=true";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-25 09:47:18 -04:00
|
|
|
|
return this._requestSummary(fetchURL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@action
|
|
|
|
|
_requestSummary(url) {
|
|
|
|
|
if (this.loading || (this.text && !this.canRegenerate)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-02 11:51:59 -04:00
|
|
|
|
this.loading = true;
|
2024-07-25 09:47:18 -04:00
|
|
|
|
this.summarizedOn = null;
|
2024-07-02 11:51:59 -04:00
|
|
|
|
|
2024-07-25 09:47:18 -04:00
|
|
|
|
return ajax(url).then((data) => {
|
2024-08-13 07:47:47 -04:00
|
|
|
|
if (data?.ai_topic_summary?.summarized_text) {
|
2024-07-02 11:51:59 -04:00
|
|
|
|
data.done = true;
|
2024-11-07 11:08:32 -05:00
|
|
|
|
this._updateSummary(data);
|
2024-07-02 11:51:59 -04:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-07 11:08:32 -05:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-02 11:51:59 -04:00
|
|
|
|
@bind
|
2024-11-07 11:08:32 -05:00
|
|
|
|
async _updateSummary(update) {
|
2024-08-29 18:07:07 -04:00
|
|
|
|
const topicSummary = {
|
|
|
|
|
done: update.done,
|
|
|
|
|
raw: update.ai_topic_summary?.summarized_text,
|
|
|
|
|
...update.ai_topic_summary,
|
|
|
|
|
};
|
2024-11-07 11:08:32 -05:00
|
|
|
|
const newText = topicSummary.raw || "";
|
2024-08-29 18:07:07 -04:00
|
|
|
|
this.loading = false;
|
|
|
|
|
|
|
|
|
|
if (update.done) {
|
2024-11-07 11:08:32 -05:00
|
|
|
|
this.text = newText;
|
|
|
|
|
this.streamedText = newText;
|
|
|
|
|
this.displayedTextLength = newText.length;
|
2024-10-21 12:15:25 -04:00
|
|
|
|
this.isStreaming = false;
|
2024-08-29 18:07:07 -04:00
|
|
|
|
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;
|
2024-11-07 11:08:32 -05:00
|
|
|
|
|
|
|
|
|
// 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();
|
2024-08-29 18:07:07 -04:00
|
|
|
|
}
|
2024-07-02 11:51:59 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-05 11:39:08 -04:00
|
|
|
|
@action
|
|
|
|
|
onRegisterApi(api) {
|
|
|
|
|
this.dMenu = api;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@action
|
|
|
|
|
async onClose() {
|
|
|
|
|
await this.dMenu.close();
|
|
|
|
|
this.unsubscribe();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-02 11:51:59 -04:00
|
|
|
|
<template>
|
2024-07-25 09:47:18 -04:00
|
|
|
|
{{#if @outletArgs.topic.summarizable}}
|
2024-07-02 11:51:59 -04:00
|
|
|
|
<div
|
2024-07-25 09:47:18 -04:00
|
|
|
|
class="ai-summarization-button"
|
2024-07-02 11:51:59 -04:00
|
|
|
|
{{didInsert this.subscribe}}
|
2024-08-13 07:47:47 -04:00
|
|
|
|
{{didUpdate this.subscribe @outletArgs.topic.id}}
|
2024-07-02 11:51:59 -04:00
|
|
|
|
{{willDestroy this.unsubscribe}}
|
|
|
|
|
>
|
2024-07-25 09:47:18 -04:00
|
|
|
|
<DMenu
|
|
|
|
|
@onShow={{this.generateSummary}}
|
2024-08-26 11:32:39 -04:00
|
|
|
|
@arrow={{false}}
|
2024-07-25 09:47:18 -04:00
|
|
|
|
@identifier="topic-map__ai-summary"
|
2024-08-05 11:39:08 -04:00
|
|
|
|
@onRegisterApi={{this.onRegisterApi}}
|
2024-07-25 09:47:18 -04:00
|
|
|
|
@interactive={{true}}
|
|
|
|
|
@triggers="click"
|
|
|
|
|
@placement="left"
|
|
|
|
|
@modalForMobile={{true}}
|
|
|
|
|
@groupIdentifier="topic-map"
|
|
|
|
|
@inline={{true}}
|
|
|
|
|
@label={{i18n "summary.buttons.generate"}}
|
|
|
|
|
@title={{i18n "summary.buttons.generate"}}
|
|
|
|
|
@icon="discourse-sparkles"
|
|
|
|
|
@triggerClass="ai-topic-summarization"
|
2024-09-18 13:36:42 -04:00
|
|
|
|
@closeOnClickOutside={{false}}
|
2024-07-25 09:47:18 -04:00
|
|
|
|
>
|
|
|
|
|
<:content>
|
|
|
|
|
<div class="ai-summary-container">
|
2024-08-05 11:39:08 -04:00
|
|
|
|
<header class="ai-summary__header">
|
|
|
|
|
<h3>{{i18n "discourse_ai.summarization.topic.title"}}</h3>
|
|
|
|
|
{{#if this.site.desktopView}}
|
|
|
|
|
<DButton
|
|
|
|
|
@title="discourse_ai.summarization.topic.close"
|
|
|
|
|
@action={{this.onClose}}
|
|
|
|
|
@icon="times"
|
|
|
|
|
@class="btn-transparent ai-summary__close"
|
|
|
|
|
/>
|
|
|
|
|
{{/if}}
|
|
|
|
|
</header>
|
|
|
|
|
|
2024-10-21 12:15:25 -04:00
|
|
|
|
<article
|
|
|
|
|
class={{concatClass
|
|
|
|
|
"ai-summary-box"
|
2024-10-22 13:55:35 -04:00
|
|
|
|
"streamable-content"
|
2024-10-21 12:15:25 -04:00
|
|
|
|
(if this.isStreaming "streaming")
|
|
|
|
|
}}
|
|
|
|
|
>
|
2024-07-25 09:47:18 -04:00
|
|
|
|
{{#if this.loading}}
|
|
|
|
|
<AiSummarySkeleton />
|
|
|
|
|
{{else}}
|
2024-10-21 12:15:25 -04:00
|
|
|
|
<div class="generated-summary cooked">
|
2024-11-07 11:08:32 -05:00
|
|
|
|
<CookText @rawText={{this.streamedText}} />
|
2024-10-21 12:15:25 -04:00
|
|
|
|
</div>
|
2024-07-25 09:47:18 -04:00
|
|
|
|
{{#if this.summarizedOn}}
|
|
|
|
|
<div class="summarized-on">
|
|
|
|
|
<p>
|
|
|
|
|
{{i18n "summary.summarized_on" date=this.summarizedOn}}
|
|
|
|
|
<DTooltip @placements={{array "top-end"}}>
|
|
|
|
|
<:trigger>
|
|
|
|
|
{{dIcon "info-circle"}}
|
|
|
|
|
</:trigger>
|
|
|
|
|
<:content>
|
|
|
|
|
{{i18n
|
|
|
|
|
"summary.model_used"
|
|
|
|
|
model=this.summarizedBy
|
|
|
|
|
}}
|
|
|
|
|
</:content>
|
|
|
|
|
</DTooltip>
|
|
|
|
|
</p>
|
|
|
|
|
<div class="outdated-summary">
|
|
|
|
|
{{#if this.outdated}}
|
|
|
|
|
<p>{{this.outdatedSummaryWarningText}}</p>
|
|
|
|
|
{{/if}}
|
|
|
|
|
{{#if this.canRegenerate}}
|
|
|
|
|
<DButton
|
|
|
|
|
@label="summary.buttons.regenerate"
|
|
|
|
|
@title="summary.buttons.regenerate"
|
|
|
|
|
@action={{this.regenerateSummary}}
|
|
|
|
|
@icon="sync"
|
|
|
|
|
/>
|
|
|
|
|
{{/if}}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-07-02 11:51:59 -04:00
|
|
|
|
{{/if}}
|
2024-07-25 09:47:18 -04:00
|
|
|
|
{{/if}}
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
</:content>
|
|
|
|
|
</DMenu>
|
2024-07-02 11:51:59 -04:00
|
|
|
|
</div>
|
|
|
|
|
{{/if}}
|
|
|
|
|
</template>
|
|
|
|
|
}
|