REFACTOR: Glimmerify topic summarization widgets. (#23043)
* REFACTOR: Glimerify topic summarization widgets. Simplifies all the logic for generating/regenerating summaries and expanding/collapsing the summary box. It makes streaming easier to implement since now we can subscribe to message bus directly from the component. * Update app/assets/javascripts/discourse/app/components/summary-box.hbs Co-authored-by: David Taylor <david@taylorhq.com> * Update app/assets/javascripts/discourse/app/components/summary-box.hbs Co-authored-by: David Taylor <david@taylorhq.com> * Update app/assets/javascripts/discourse/app/components/summary-box.hbs Co-authored-by: David Taylor <david@taylorhq.com> --------- Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
parent
0b29dc5d38
commit
b832786f35
|
@ -1,4 +1,5 @@
|
|||
<ul class="ai-summary__list" {{did-insert this.setupAnimation}}>
|
||||
<div class="ai-summary__container">
|
||||
<ul class="ai-summary__list" {{did-insert this.setupAnimation}}>
|
||||
{{#each this.blocks as |block|}}
|
||||
<li
|
||||
class={{concat-class
|
||||
|
@ -12,9 +13,9 @@
|
|||
{{will-destroy this.teardownAnimation}}
|
||||
></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<span>
|
||||
<span>
|
||||
<div class="ai-summary__generating-text">
|
||||
{{i18n "summary.in_progress"}}
|
||||
</div>
|
||||
|
@ -23,4 +24,5 @@
|
|||
<span class="ai-summary__indicator-dot">.</span>
|
||||
<span class="ai-summary__indicator-dot">.</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
|
@ -0,0 +1,62 @@
|
|||
{{#if @postAttrs.hasTopRepliesSummary}}
|
||||
<p>{{html-safe this.topRepliesSummaryInfo}}</p>
|
||||
{{/if}}
|
||||
<div class="summarization-buttons">
|
||||
{{#if @postAttrs.summarizable}}
|
||||
{{#if this.canCollapseSummary}}
|
||||
<DButton
|
||||
@class="btn-primary topic-strategy-summarization"
|
||||
@action={{this.toggleSummary}}
|
||||
@title="summary.buttons.hide"
|
||||
@label="summary.buttons.hide"
|
||||
@icon="chevron-up"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@class="btn-primary topic-strategy-summarization"
|
||||
@action={{this.generateSummary}}
|
||||
@translatedLabel={{this.generateSummaryTitle}}
|
||||
@translatedTitle={{this.generateSummaryTitle}}
|
||||
@icon={{this.generateSummaryIcon}}
|
||||
@disabled={{this.loadingSummary}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @postAttrs.hasTopRepliesSummary}}
|
||||
<DButton
|
||||
@class="top-replies"
|
||||
@action={{this.toggleTopRepliesFilter}}
|
||||
@translatedTitle={{this.topRepliesTitle}}
|
||||
@translatedLabel={{this.topRepliesLabel}}
|
||||
@icon={{this.topRepliesIcon}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.showSummaryBox}}
|
||||
<article class="summary-box">
|
||||
{{#if this.loadingSummary}}
|
||||
<AiSummarySkeleton />
|
||||
{{else}}
|
||||
<div class="generated-summary">{{this.summary}}</div>
|
||||
<div class="summarized-on">
|
||||
<p>
|
||||
{{i18n "summary.summarized_on" date=this.summarizedOn}}
|
||||
<span>
|
||||
{{d-icon "info-circle"}}
|
||||
<DTooltip @placement="top-end">
|
||||
{{i18n "summary.model_used" model=this.summarizedBy}}
|
||||
</DTooltip>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{{#if this.outdated}}
|
||||
<p class="outdated-summary">
|
||||
{{this.outdatedSummaryWarningText}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</article>
|
||||
{{/if}}
|
|
@ -0,0 +1,156 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import I18n from "I18n";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { cookAsync } from "discourse/lib/text";
|
||||
import { shortDateNoYear } from "discourse/lib/formatter";
|
||||
|
||||
const MIN_POST_READ_TIME = 4;
|
||||
|
||||
export default class SummaryBox extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
@tracked summary = "";
|
||||
@tracked summarizedOn = null;
|
||||
@tracked summarizedBy = null;
|
||||
@tracked newPostsSinceSummary = null;
|
||||
@tracked outdated = false;
|
||||
@tracked canRegenerate = false;
|
||||
|
||||
@tracked regenerated = false;
|
||||
@tracked showSummaryBox = false;
|
||||
@tracked canCollapseSummary = false;
|
||||
@tracked loadingSummary = false;
|
||||
|
||||
get generateSummaryTitle() {
|
||||
const title = this.canRegenerate
|
||||
? "summary.buttons.regenerate"
|
||||
: "summary.buttons.generate";
|
||||
|
||||
return I18n.t(title);
|
||||
}
|
||||
|
||||
get generateSummaryIcon() {
|
||||
return this.canRegenerate ? "sync" : "magic";
|
||||
}
|
||||
|
||||
get outdatedSummaryWarningText() {
|
||||
let outdatedText = I18n.t("summary.outdated");
|
||||
|
||||
if (
|
||||
!this.args.postAttrs.hasTopRepliesSummary &&
|
||||
this.newPostsSinceSummary > 0
|
||||
) {
|
||||
outdatedText += " ";
|
||||
outdatedText += I18n.t("summary.outdated_posts", {
|
||||
count: this.newPostsSinceSummary,
|
||||
});
|
||||
}
|
||||
|
||||
return outdatedText;
|
||||
}
|
||||
|
||||
get topRepliesSummaryEnabled() {
|
||||
return this.args.postAttrs.topicSummaryEnabled;
|
||||
}
|
||||
|
||||
get topRepliesSummaryInfo() {
|
||||
if (this.args.postAttrs.topicSummaryEnabled) {
|
||||
return I18n.t("summary.enabled_description");
|
||||
}
|
||||
|
||||
const wordCount = this.args.postAttrs.topicWordCount;
|
||||
if (wordCount && this.siteSettings.read_time_word_count > 0) {
|
||||
const readingTime = Math.ceil(
|
||||
Math.max(
|
||||
wordCount / this.siteSettings.read_time_word_count,
|
||||
(this.args.postAttrs.topicPostsCount * MIN_POST_READ_TIME) / 60
|
||||
)
|
||||
);
|
||||
return I18n.messageFormat("summary.description_time_MF", {
|
||||
replyCount: this.args.postAttrs.topicReplyCount,
|
||||
readingTime,
|
||||
});
|
||||
}
|
||||
return I18n.t("summary.description", {
|
||||
count: this.args.postAttrs.topicReplyCount,
|
||||
});
|
||||
}
|
||||
|
||||
get topRepliesTitle() {
|
||||
if (this.topRepliesSummaryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return I18n.t("summary.short_title");
|
||||
}
|
||||
|
||||
get topRepliesLabel() {
|
||||
const label = this.topRepliesSummaryEnabled
|
||||
? "summary.disable"
|
||||
: "summary.enable";
|
||||
|
||||
return I18n.t(label);
|
||||
}
|
||||
|
||||
get topRepliesIcon() {
|
||||
if (this.topRepliesSummaryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return "layer-group";
|
||||
}
|
||||
|
||||
@action
|
||||
toggleTopRepliesFilter() {
|
||||
const filterFunction = this.topRepliesSummaryEnabled
|
||||
? "cancelFilter"
|
||||
: "showTopReplies";
|
||||
|
||||
this.args.topRepliesToggle(filterFunction);
|
||||
}
|
||||
|
||||
@action
|
||||
collapseSummary() {
|
||||
this.showSummaryBox = false;
|
||||
this.canCollapseSummary = false;
|
||||
}
|
||||
|
||||
@action
|
||||
generateSummary() {
|
||||
this.showSummaryBox = true;
|
||||
|
||||
if (this.summary && !this.canRegenerate) {
|
||||
this.canCollapseSummary = true;
|
||||
return;
|
||||
} else {
|
||||
this.loadingSummary = true;
|
||||
}
|
||||
|
||||
let fetchURL = `/t/${this.args.postAttrs.topicId}/strategy-summary`;
|
||||
|
||||
if (this.canRegenerate) {
|
||||
fetchURL += "?skip_age_check=true";
|
||||
}
|
||||
|
||||
ajax(fetchURL)
|
||||
.then((data) => {
|
||||
cookAsync(data.summary).then((cooked) => {
|
||||
this.summary = cooked;
|
||||
this.summarizedOn = shortDateNoYear(data.summarized_on);
|
||||
this.summarizedBy = data.summarized_by;
|
||||
this.newPostsSinceSummary = data.new_posts_since_summary;
|
||||
this.outdated = data.outdated;
|
||||
this.newPostsSinceSummary = data.new_posts_since_summary;
|
||||
this.canRegenerate = data.outdated && data.can_regenerate;
|
||||
|
||||
this.canCollapseSummary = !this.canRegenerate;
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => (this.loadingSummary = false));
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { cookAsync } from "discourse/lib/text";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import I18n from "I18n";
|
||||
import { shortDateNoYear } from "discourse/lib/formatter";
|
||||
import { h } from "virtual-dom";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
|
||||
export default createWidget("summary-box", {
|
||||
tagName: "article.summary-box",
|
||||
buildKey: (attrs) => `summary-box-${attrs.topicId}`,
|
||||
|
||||
defaultState() {
|
||||
return { expandSummarizedOn: false };
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const html = [];
|
||||
|
||||
const summary = attrs.summary;
|
||||
|
||||
if (summary && !attrs.skipAgeCheck) {
|
||||
html.push(
|
||||
new RawHtml({
|
||||
html: `<div class="generated-summary">${summary.summarized_text}</div>`,
|
||||
})
|
||||
);
|
||||
|
||||
const summarizationInfo = [
|
||||
h("p", {}, [
|
||||
I18n.t("summary.summarized_on", { date: summary.summarized_on }),
|
||||
this.buildTooltip(attrs),
|
||||
]),
|
||||
];
|
||||
|
||||
if (summary.outdated) {
|
||||
summarizationInfo.push(this.outdatedSummaryWarning(attrs));
|
||||
}
|
||||
|
||||
html.push(h("div.summarized-on", {}, summarizationInfo));
|
||||
} else {
|
||||
html.push(this.buildSummarySkeleton());
|
||||
this.fetchSummary(attrs.topicId, attrs.skipAgeCheck);
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
buildSummarySkeleton() {
|
||||
return new RenderGlimmer(
|
||||
this,
|
||||
"div.ai-summary__container",
|
||||
hbs`{{ai-summary-skeleton}}`
|
||||
);
|
||||
},
|
||||
|
||||
buildTooltip(attrs) {
|
||||
return new RenderGlimmer(
|
||||
this,
|
||||
"span",
|
||||
hbs`{{d-icon "info-circle"}}<DTooltip @placement="top-end">
|
||||
{{i18n "summary.model_used" model=@data.summarizedBy}}
|
||||
</DTooltip>`,
|
||||
{
|
||||
summarizedBy: attrs.summary.summarized_by,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
outdatedSummaryWarning(attrs) {
|
||||
let outdatedText = I18n.t("summary.outdated");
|
||||
|
||||
if (
|
||||
!attrs.hasTopRepliesSummary &&
|
||||
attrs.summary.new_posts_since_summary > 0
|
||||
) {
|
||||
outdatedText += " ";
|
||||
outdatedText += I18n.t("summary.outdated_posts", {
|
||||
count: attrs.summary.new_posts_since_summary,
|
||||
});
|
||||
}
|
||||
|
||||
return h("p.outdated-summary", {}, [
|
||||
outdatedText,
|
||||
iconNode("exclamation-triangle", { class: "info-icon" }),
|
||||
]);
|
||||
},
|
||||
|
||||
fetchSummary(topicId, skipAgeCheck) {
|
||||
let fetchURL = `/t/${topicId}/strategy-summary`;
|
||||
|
||||
if (skipAgeCheck) {
|
||||
fetchURL += "?skip_age_check=true";
|
||||
}
|
||||
|
||||
ajax(fetchURL)
|
||||
.then((data) => {
|
||||
cookAsync(data.summary).then((cooked) => {
|
||||
// We store the summary in the parent so we can re-render it without doing a new request.
|
||||
data.summarized_text = cooked.string;
|
||||
data.summarized_on = shortDateNoYear(data.summarized_on);
|
||||
|
||||
if (skipAgeCheck) {
|
||||
data.regenerated = true;
|
||||
}
|
||||
|
||||
this.sendWidgetEvent("summaryUpdated", data);
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
});
|
|
@ -1,154 +0,0 @@
|
|||
import I18n from "I18n";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
|
||||
const MIN_POST_READ_TIME = 4;
|
||||
|
||||
createWidget("toggle-summary-description", {
|
||||
description(attrs) {
|
||||
if (attrs.topicSummaryEnabled) {
|
||||
return I18n.t("summary.enabled_description");
|
||||
}
|
||||
|
||||
if (attrs.topicWordCount && this.siteSettings.read_time_word_count > 0) {
|
||||
const readingTime = Math.ceil(
|
||||
Math.max(
|
||||
attrs.topicWordCount / this.siteSettings.read_time_word_count,
|
||||
(attrs.topicPostsCount * MIN_POST_READ_TIME) / 60
|
||||
)
|
||||
);
|
||||
return I18n.messageFormat("summary.description_time_MF", {
|
||||
replyCount: attrs.topicReplyCount,
|
||||
readingTime,
|
||||
});
|
||||
}
|
||||
return I18n.t("summary.description", { count: attrs.topicReplyCount });
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
// vdom makes putting html in the i18n difficult
|
||||
return new RawHtml({ html: `<p>${this.description(attrs)}</p>` });
|
||||
},
|
||||
});
|
||||
|
||||
export default createWidget("toggle-topic-summary", {
|
||||
tagName: "section.information.toggle-summary",
|
||||
buildKey: (attrs) => `toggle-topic-summary-${attrs.topicId}`,
|
||||
|
||||
defaultState() {
|
||||
return {
|
||||
expandSummaryBox: false,
|
||||
summaryBoxHidden: true,
|
||||
summary: "",
|
||||
summarizedOn: null,
|
||||
summarizedBy: null,
|
||||
};
|
||||
},
|
||||
|
||||
html(attrs, state) {
|
||||
const html = [];
|
||||
const summarizationButtons = [];
|
||||
|
||||
if (attrs.summarizable) {
|
||||
const canRegenerate =
|
||||
!state.regenerate &&
|
||||
state.summary.outdated &&
|
||||
state.summary.can_regenerate;
|
||||
const canCollapse =
|
||||
!canRegenerate && !this.loadingSummary() && this.summaryBoxVisble();
|
||||
const summarizeButton = canCollapse
|
||||
? this.hideSummaryButton()
|
||||
: this.generateSummaryButton(canRegenerate);
|
||||
|
||||
summarizationButtons.push(summarizeButton);
|
||||
}
|
||||
|
||||
if (attrs.hasTopRepliesSummary) {
|
||||
html.push(this.attach("toggle-summary-description", attrs));
|
||||
summarizationButtons.push(
|
||||
this.attach("button", {
|
||||
className: "btn top-replies",
|
||||
icon: attrs.topicSummaryEnabled ? null : "layer-group",
|
||||
title: attrs.topicSummaryEnabled ? null : "summary.short_title",
|
||||
label: attrs.topicSummaryEnabled
|
||||
? "summary.disable"
|
||||
: "summary.enable",
|
||||
action: attrs.topicSummaryEnabled ? "cancelFilter" : "showTopReplies",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (summarizationButtons) {
|
||||
html.push(h("div.summarization-buttons", summarizationButtons));
|
||||
}
|
||||
|
||||
if (this.summaryBoxVisble()) {
|
||||
attrs.summary = state.summary;
|
||||
attrs.skipAgeCheck = state.regenerate;
|
||||
|
||||
html.push(this.attach("summary-box", attrs));
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
generateSummaryButton(canRegenerate) {
|
||||
const title = canRegenerate
|
||||
? "summary.buttons.regenerate"
|
||||
: "summary.buttons.generate";
|
||||
const icon = canRegenerate ? "sync" : "magic";
|
||||
|
||||
return this.attach("button", {
|
||||
className: "btn btn-primary topic-strategy-summarization",
|
||||
icon,
|
||||
title: I18n.t(title),
|
||||
translatedTitle: I18n.t(title),
|
||||
translatedLabel: I18n.t(title),
|
||||
action: canRegenerate ? "regenerateSummary" : "expandSummaryBox",
|
||||
disabled: this.loadingSummary(),
|
||||
});
|
||||
},
|
||||
|
||||
hideSummaryButton() {
|
||||
return this.attach("button", {
|
||||
className: "btn btn-primary topic-strategy-summarization",
|
||||
icon: "chevron-up",
|
||||
title: "summary.buttons.hide",
|
||||
label: "summary.buttons.hide",
|
||||
action: "toggleSummaryBox",
|
||||
disabled: this.loadingSummary(),
|
||||
});
|
||||
},
|
||||
|
||||
loadingSummary() {
|
||||
return (
|
||||
this.summaryBoxVisble() && (!this.state.summary || this.state.regenerate)
|
||||
);
|
||||
},
|
||||
|
||||
summaryUpdatedEvent(summary) {
|
||||
this.state.summary = summary;
|
||||
|
||||
if (summary.regenerated) {
|
||||
this.state.regenerate = false;
|
||||
}
|
||||
},
|
||||
|
||||
summaryBoxVisble() {
|
||||
return this.state.expandSummaryBox && !this.state.summaryBoxHidden;
|
||||
},
|
||||
|
||||
expandSummaryBox() {
|
||||
this.state.expandSummaryBox = true;
|
||||
this.state.summaryBoxHidden = false;
|
||||
},
|
||||
|
||||
regenerateSummary() {
|
||||
this.state.regenerate = true;
|
||||
},
|
||||
|
||||
toggleSummaryBox() {
|
||||
this.state.summaryBoxHidden = !this.state.summaryBoxHidden;
|
||||
},
|
||||
});
|
|
@ -6,6 +6,8 @@ import { h } from "virtual-dom";
|
|||
import { replaceEmoji } from "discourse/widgets/emoji";
|
||||
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
|
||||
const LINKS_SHOWN = 5;
|
||||
|
||||
|
@ -387,7 +389,7 @@ export default createWidget("topic-map", {
|
|||
}
|
||||
|
||||
if (attrs.hasTopRepliesSummary || attrs.summarizable) {
|
||||
contents.push(this.attach("toggle-topic-summary", attrs));
|
||||
contents.push(this.buildSummaryBox(attrs));
|
||||
}
|
||||
|
||||
if (attrs.showPMMap) {
|
||||
|
@ -399,4 +401,21 @@ export default createWidget("topic-map", {
|
|||
toggleMap() {
|
||||
this.state.collapsed = !this.state.collapsed;
|
||||
},
|
||||
|
||||
buildSummaryBox(attrs) {
|
||||
return new RenderGlimmer(
|
||||
this,
|
||||
"section.information.toggle-summary",
|
||||
hbs`<SummaryBox
|
||||
@postAttrs={{@data.postAttrs}}
|
||||
@topRepliesToggle={{@data.actionDispatchFunc}}
|
||||
/>`,
|
||||
{
|
||||
postAttrs: attrs,
|
||||
actionDispatchFunc: (actionName) => {
|
||||
this.sendWidgetAction(actionName);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue