From b832786f3563de14fb78916d21624e25d8c13d9a Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Wed, 9 Aug 2023 17:11:24 -0300 Subject: [PATCH] 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 * Update app/assets/javascripts/discourse/app/components/summary-box.hbs Co-authored-by: David Taylor * Update app/assets/javascripts/discourse/app/components/summary-box.hbs Co-authored-by: David Taylor --------- Co-authored-by: David Taylor --- .../app/components/ai-summary-skeleton.hbs | 50 +++--- .../discourse/app/components/summary-box.hbs | 62 +++++++ .../discourse/app/components/summary-box.js | 156 ++++++++++++++++++ .../discourse/app/widgets/summary-box.js | 116 ------------- .../app/widgets/toggle-topic-summary.js | 154 ----------------- .../discourse/app/widgets/topic-map.js | 21 ++- 6 files changed, 264 insertions(+), 295 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/summary-box.hbs create mode 100644 app/assets/javascripts/discourse/app/components/summary-box.js delete mode 100644 app/assets/javascripts/discourse/app/widgets/summary-box.js delete mode 100644 app/assets/javascripts/discourse/app/widgets/toggle-topic-summary.js diff --git a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs index 410f6376e0b..528b6460598 100644 --- a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs +++ b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs @@ -1,26 +1,28 @@ -
    - {{#each this.blocks as |block|}} -
  • - {{/each}} -
+
+
    + {{#each this.blocks as |block|}} +
  • + {{/each}} +
- -
- {{i18n "summary.in_progress"}} -
- - . - . - . + +
+ {{i18n "summary.in_progress"}} +
+ + . + . + . +
-
\ No newline at end of file +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/summary-box.hbs b/app/assets/javascripts/discourse/app/components/summary-box.hbs new file mode 100644 index 00000000000..cc1d2223801 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/summary-box.hbs @@ -0,0 +1,62 @@ +{{#if @postAttrs.hasTopRepliesSummary}} +

{{html-safe this.topRepliesSummaryInfo}}

+{{/if}} +
+ {{#if @postAttrs.summarizable}} + {{#if this.canCollapseSummary}} + + {{else}} + + {{/if}} + {{/if}} + + {{#if @postAttrs.hasTopRepliesSummary}} + + {{/if}} +
+ +{{#if this.showSummaryBox}} +
+ {{#if this.loadingSummary}} + + {{else}} +
{{this.summary}}
+
+

+ {{i18n "summary.summarized_on" date=this.summarizedOn}} + + {{d-icon "info-circle"}} + + {{i18n "summary.model_used" model=this.summarizedBy}} + + +

+ + {{#if this.outdated}} +

+ {{this.outdatedSummaryWarningText}} +

+ {{/if}} +
+ {{/if}} +
+{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/summary-box.js b/app/assets/javascripts/discourse/app/components/summary-box.js new file mode 100644 index 00000000000..2de660f81b7 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/summary-box.js @@ -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)); + } +} diff --git a/app/assets/javascripts/discourse/app/widgets/summary-box.js b/app/assets/javascripts/discourse/app/widgets/summary-box.js deleted file mode 100644 index fa0e425a1b6..00000000000 --- a/app/assets/javascripts/discourse/app/widgets/summary-box.js +++ /dev/null @@ -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: `
${summary.summarized_text}
`, - }) - ); - - 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"}} - {{i18n "summary.model_used" model=@data.summarizedBy}} - `, - { - 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); - }, -}); diff --git a/app/assets/javascripts/discourse/app/widgets/toggle-topic-summary.js b/app/assets/javascripts/discourse/app/widgets/toggle-topic-summary.js deleted file mode 100644 index fef771cc617..00000000000 --- a/app/assets/javascripts/discourse/app/widgets/toggle-topic-summary.js +++ /dev/null @@ -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: `

${this.description(attrs)}

` }); - }, -}); - -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; - }, -}); diff --git a/app/assets/javascripts/discourse/app/widgets/topic-map.js b/app/assets/javascripts/discourse/app/widgets/topic-map.js index 67d6bdd323d..6f9decfb0b3 100644 --- a/app/assets/javascripts/discourse/app/widgets/topic-map.js +++ b/app/assets/javascripts/discourse/app/widgets/topic-map.js @@ -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``, + { + postAttrs: attrs, + actionDispatchFunc: (actionName) => { + this.sendWidgetAction(actionName); + }, + } + ); + }, });