diff --git a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs deleted file mode 100644 index 528b6460598..00000000000 --- a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs +++ /dev/null @@ -1,28 +0,0 @@ -
- - - -
- {{i18n "summary.in_progress"}} -
- - . - . - . - -
-
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.js b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.js deleted file mode 100644 index e362dfbb3db..00000000000 --- a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.js +++ /dev/null @@ -1,92 +0,0 @@ -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { cancel } from "@ember/runloop"; -import discourseLater from "discourse-common/lib/later"; - -class Block { - @tracked show = false; - @tracked shown = false; - @tracked blinking = false; - - constructor(args = {}) { - this.show = args.show ?? false; - this.shown = args.shown ?? false; - } -} - -const BLOCKS_SIZE = 20; // changing this requires to change css accordingly - -export default class AiSummarySkeleton extends Component { - blocks = [...Array.from({ length: BLOCKS_SIZE }, () => new Block())]; - - #nextBlockBlinkingTimer; - #blockBlinkingTimer; - #blockShownTimer; - - @action - setupAnimation() { - this.blocks.firstObject.show = true; - this.blocks.firstObject.shown = true; - } - - @action - onBlinking(block) { - if (!block.blinking) { - return; - } - - block.show = false; - - this.#nextBlockBlinkingTimer = discourseLater( - this, - () => { - this.#nextBlock(block).blinking = true; - }, - 250 - ); - - this.#blockBlinkingTimer = discourseLater( - this, - () => { - block.blinking = false; - }, - 500 - ); - } - - @action - onShowing(block) { - if (!block.show) { - return; - } - - this.#blockShownTimer = discourseLater( - this, - () => { - this.#nextBlock(block).show = true; - this.#nextBlock(block).shown = true; - - if (this.blocks.lastObject === block) { - this.blocks.firstObject.blinking = true; - } - }, - 250 - ); - } - - @action - teardownAnimation() { - cancel(this.#blockShownTimer); - cancel(this.#nextBlockBlinkingTimer); - cancel(this.#blockBlinkingTimer); - } - - #nextBlock(currentBlock) { - if (currentBlock === this.blocks.lastObject) { - return this.blocks.firstObject; - } else { - return this.blocks.objectAt(this.blocks.indexOf(currentBlock) + 1); - } - } -} diff --git a/app/assets/javascripts/discourse/app/components/summary-box.hbs b/app/assets/javascripts/discourse/app/components/summary-box.hbs deleted file mode 100644 index 15f4ea2823d..00000000000 --- a/app/assets/javascripts/discourse/app/components/summary-box.hbs +++ /dev/null @@ -1,55 +0,0 @@ -
- {{#if @topic.summarizable}} - {{#if this.summary.showSummaryBox}} - - {{else}} - - {{/if}} - {{/if}} - - {{#if this.summary.showSummaryBox}} -
- {{#if (and this.summary.loading (not this.summary.text))}} - - {{else}} -
{{this.summary.text}}
- - {{#if this.summary.summarizedOn}} -
-

- {{i18n "summary.summarized_on" date=this.summary.summarizedOn}} - - - <:trigger> - {{d-icon "info-circle"}} - - <:content> - {{i18n "summary.model_used" model=this.summary.summarizedBy}} - - -

- - {{#if this.summary.outdated}} -

- {{this.outdatedSummaryWarningText}} -

- {{/if}} -
- {{/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 deleted file mode 100644 index 298071410de..00000000000 --- a/app/assets/javascripts/discourse/app/components/summary-box.js +++ /dev/null @@ -1,43 +0,0 @@ -import Component from "@glimmer/component"; -import { service } from "@ember/service"; -import I18n from "discourse-i18n"; - -export default class SummaryBox extends Component { - @service siteSettings; - - get summary() { - return this.args.postStream.topicSummary; - } - - get generateSummaryTitle() { - const title = this.summary.canRegenerate - ? "summary.buttons.regenerate" - : "summary.buttons.generate"; - - return I18n.t(title); - } - - get generateSummaryIcon() { - return this.summary.canRegenerate ? "sync" : "discourse-sparkles"; - } - - get outdatedSummaryWarningText() { - let outdatedText = I18n.t("summary.outdated"); - - if ( - !this.topRepliesSummaryEnabled && - this.summary.newPostsSinceSummary > 0 - ) { - outdatedText += " "; - outdatedText += I18n.t("summary.outdated_posts", { - count: this.summary.newPostsSinceSummary, - }); - } - - return outdatedText; - } - - get topRepliesSummaryEnabled() { - return this.args.postStream.summary; - } -} diff --git a/app/assets/javascripts/discourse/app/components/topic-map.gjs b/app/assets/javascripts/discourse/app/components/topic-map.gjs index 99c0d187596..2cc6ef3a8ac 100644 --- a/app/assets/javascripts/discourse/app/components/topic-map.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-map.gjs @@ -6,13 +6,11 @@ import { service } from "@ember/service"; import { htmlSafe } from "@ember/template"; import DButton from "discourse/components/d-button"; import PluginOutlet from "discourse/components/plugin-outlet"; -import SummaryBox from "discourse/components/summary-box"; import PrivateMessageMap from "discourse/components/topic-map/private-message-map"; import TopicMapExpanded from "discourse/components/topic-map/topic-map-expanded"; import TopicMapSummary from "discourse/components/topic-map/topic-map-summary"; import concatClass from "discourse/helpers/concat-class"; import I18n from "discourse-i18n"; -import or from "truth-helpers/helpers/or"; const MIN_POST_READ_TIME = 4; @@ -98,41 +96,26 @@ export default class TopicMap extends Component { {{/unless}} - {{#if (or @model.has_summary @model.summarizable)}} -
+
+ {{#if @model.has_summary}} +

{{htmlSafe this.topRepliesSummaryInfo}}

+ {{/if}} + {{#if @model.has_summary}} -

{{htmlSafe this.topRepliesSummaryInfo}}

+ {{/if}} - -
- {{#if @model.has_summary}} - - {{/if}} -
- - {{#if @model.summarizable}} - - {{/if}} -
-
- {{/if}} + +
{{#if @showPMMap}}
diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index 4facfe81b39..fe98d7249a9 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -568,14 +568,6 @@ export default Controller.extend(bufferedProperty("model"), { }); }, - collapseSummary() { - this.get("model.postStream").collapseSummary(); - }, - - showSummary() { - this.get("model.postStream").showSummary(this.currentUser); - }, - removeAllowedUser(user) { return this.get("model.details") .removeAllowedUser(user) @@ -1649,9 +1641,6 @@ export default Controller.extend(bufferedProperty("model"), { this.onMessage, this.get("model.message_bus_last_id") ); - - const summariesChannel = `/summaries/topic/${this.get("model.id")}`; - this.messageBus.subscribe(summariesChannel, this._updateSummary); }, unsubscribe() { @@ -1661,13 +1650,6 @@ export default Controller.extend(bufferedProperty("model"), { } this.messageBus.unsubscribe("/topic/*", this.onMessage); - this.messageBus.unsubscribe("/summaries/topic/*", this._updateSummary); - }, - - @bind - _updateSummary(update) { - const postStream = this.get("model.postStream"); - postStream.processSummaryUpdate(update); }, @bind diff --git a/app/assets/javascripts/discourse/app/lib/topic-summary.js b/app/assets/javascripts/discourse/app/lib/topic-summary.js deleted file mode 100644 index bcced963bf5..00000000000 --- a/app/assets/javascripts/discourse/app/lib/topic-summary.js +++ /dev/null @@ -1,72 +0,0 @@ -import { tracked } from "@glimmer/tracking"; -import { ajax } from "discourse/lib/ajax"; -import { shortDateNoYear } from "discourse/lib/formatter"; -import { cook } from "discourse/lib/text"; - -export default class TopicSummary { - @tracked text = ""; - @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; - - processUpdate(update) { - const topicSummary = update.topic_summary; - - return cook(topicSummary.summarized_text) - .then((cooked) => { - this.text = cooked; - this.loading = false; - }) - .then(() => { - if (update.done) { - this.summarizedOn = shortDateNoYear(topicSummary.summarized_on); - this.summarizedBy = topicSummary.algorithm; - this.newPostsSinceSummary = topicSummary.new_posts_since_summary; - this.outdated = topicSummary.outdated; - this.newPostsSinceSummary = topicSummary.new_posts_since_summary; - this.canRegenerate = - topicSummary.outdated && topicSummary.can_regenerate; - } - }); - } - - collapse() { - this.showSummaryBox = false; - this.canCollapseSummary = false; - } - - generateSummary(currentUser, topicId) { - this.showSummaryBox = true; - - if (this.text && !this.canRegenerate) { - this.canCollapseSummary = false; - return; - } - - let fetchURL = `/t/${topicId}/strategy-summary?`; - - if (currentUser) { - fetchURL += `stream=true`; - - if (this.canRegenerate) { - fetchURL += "&skip_age_check=true"; - } - } - - this.loading = true; - - return ajax(fetchURL).then((data) => { - if (!currentUser) { - data.done = true; - this.processUpdate(data); - } - }); - } -} diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js index 7a124ea5be9..6315fd1e88c 100644 --- a/app/assets/javascripts/discourse/app/models/post-stream.js +++ b/app/assets/javascripts/discourse/app/models/post-stream.js @@ -6,7 +6,6 @@ import { isEmpty } from "@ember/utils"; import { Promise } from "rsvp"; import { ajax } from "discourse/lib/ajax"; import PostsWithPlaceholders from "discourse/lib/posts-with-placeholders"; -import TopicSummary from "discourse/lib/topic-summary"; import DiscourseURL from "discourse/lib/url"; import { highlightPost } from "discourse/lib/utilities"; import RestModel from "discourse/models/rest"; @@ -51,7 +50,6 @@ export default class PostStream extends RestModel { filterRepliesToPostNumber = null; filterUpwardsPostID = null; filter = null; - topicSummary = null; lastId = null; @or("loadingAbove", "loadingBelow", "loadingFilter", "stagingPost") loading; @@ -86,7 +84,6 @@ export default class PostStream extends RestModel { loadingFilter: false, stagingPost: false, timelineLookup: [], - topicSummary: new TopicSummary(), }); } @@ -1259,18 +1256,6 @@ export default class PostStream extends RestModel { } } - collapseSummary() { - this.topicSummary.collapse(); - } - - showSummary(currentUser) { - this.topicSummary.generateSummary(currentUser, this.get("topic.id")); - } - - processSummaryUpdate(update) { - this.topicSummary.processUpdate(update); - } - _initUserModels(post) { post.user = this.store.createRecord("user", { id: post.user_id, diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs index 1c721233b91..f08a8170429 100644 --- a/app/assets/javascripts/discourse/app/templates/topic.hbs +++ b/app/assets/javascripts/discourse/app/templates/topic.hbs @@ -361,8 +361,6 @@ @toggleWiki={{action "toggleWiki"}} @showTopReplies={{action "showTopReplies"}} @cancelFilter={{action "cancelFilter"}} - @collapseSummary={{action "collapseSummary"}} - @showSummary={{action "showSummary"}} @removeAllowedUser={{action "removeAllowedUser"}} @removeAllowedGroup={{action "removeAllowedGroup"}} @topVisibleChanged={{action "topVisibleChanged"}} diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js index 69a2e7591c7..21b950d4bf9 100644 --- a/app/assets/javascripts/discourse/app/widgets/post.js +++ b/app/assets/javascripts/discourse/app/widgets/post.js @@ -761,8 +761,6 @@ createWidget("post-body", { @showPMMap={{@data.showPMMap}} @cancelFilter={{@data.cancelFilter}} @showTopReplies={{@data.showTopReplies}} - @collapseSummary={{@data.collapseSummary}} - @showSummary={{@data.showSummary}} @showInvite={{@data.showInvite}} @removeAllowedGroup={{@data.removeAllowedGroup}} @removeAllowedUser={{@data.removeAllowedUser}} @@ -774,8 +772,6 @@ createWidget("post-body", { showPMMap: attrs.showPMMap, cancelFilter: () => this.sendWidgetAction("cancelFilter"), showTopReplies: () => this.sendWidgetAction("showTopReplies"), - collapseSummary: () => this.sendWidgetAction("collapseSummary"), - showSummary: () => this.sendWidgetAction("showSummary"), showInvite: () => this.sendWidgetAction("showInvite"), removeAllowedGroup: (group) => this.sendWidgetAction("removeAllowedGroup", group), diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-summary-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-summary-test.js deleted file mode 100644 index 2bd4b3edd21..00000000000 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-summary-test.js +++ /dev/null @@ -1,106 +0,0 @@ -import { click, visit } from "@ember/test-helpers"; -import { test } from "qunit"; -import topicFixtures from "discourse/tests/fixtures/topic"; -import { - acceptance, - publishToMessageBus, - updateCurrentUser, -} from "discourse/tests/helpers/qunit-helpers"; -import { cloneJSON } from "discourse-common/lib/object"; - -acceptance("Topic - Summary", function (needs) { - const currentUserId = 5; - - needs.user(); - needs.pretender((server, helper) => { - server.get("/t/1.json", () => { - const json = cloneJSON(topicFixtures["/t/130.json"]); - json.id = 1; - json.summarizable = true; - - return helper.response(json); - }); - - server.get("/t/1/strategy-summary", () => { - return helper.response({}); - }); - }); - - needs.hooks.beforeEach(() => { - updateCurrentUser({ id: currentUserId }); - }); - - test("displays streamed summary", async function (assert) { - await visit("/t/-/1"); - - const partialSummary = "This a"; - await publishToMessageBus("/summaries/topic/1", { - done: false, - topic_summary: { summarized_text: partialSummary }, - }); - - await click(".topic-strategy-summarization"); - - assert - .dom(".summary-box .generated-summary p") - .hasText(partialSummary, "Updates the summary with a partial result"); - - const finalSummary = "This is a completed summary"; - await publishToMessageBus("/summaries/topic/1", { - done: true, - topic_summary: { - summarized_text: finalSummary, - summarized_on: "2023-01-01T04:00:00.000Z", - algorithm: "OpenAI GPT-4", - outdated: false, - new_posts_since_summary: false, - can_regenerate: true, - }, - }); - - assert - .dom(".summary-box .generated-summary p") - .hasText(finalSummary, "Updates the summary with a final result"); - - assert.dom(".summary-box .summarized-on").exists("summary metadata exists"); - }); -}); - -acceptance("Topic - Summary - Anon", function (needs) { - const finalSummary = "This is a completed summary"; - - needs.pretender((server, helper) => { - server.get("/t/1.json", () => { - const json = cloneJSON(topicFixtures["/t/280/1.json"]); - json.id = 1; - json.summarizable = true; - - return helper.response(json); - }); - - server.get("/t/1/strategy-summary", () => { - return helper.response({ - topic_summary: { - summarized_text: finalSummary, - summarized_on: "2023-01-01T04:00:00.000Z", - algorithm: "OpenAI GPT-4", - outdated: false, - new_posts_since_summary: false, - can_regenerate: false, - }, - }); - }); - }); - - test("displays cached summary inmediately", async function (assert) { - await visit("/t/-/1"); - - await click(".topic-strategy-summarization"); - - assert - .dom(".summary-box .generated-summary p") - .hasText(finalSummary, "Updates the summary with the result"); - - assert.dom(".summary-box .summarized-on").exists("summary metadata exists"); - }); -}); diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js index 2913dbe3e31..b9511ffcca3 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js @@ -853,14 +853,14 @@ module("Integration | Component | Widget | post", function (hooks) { assert.dom(".topic-map-expanded .topic-link").exists({ count: 6 }); }); - test("topic map - no summary", async function (assert) { + test("topic map - no top reply summary", async function (assert) { const store = getOwner(this).lookup("service:store"); const topic = store.createRecord("topic", { id: 123 }); this.set("args", { topic, showTopicMap: true }); await render(hbs``); - assert.dom(".toggle-summary").doesNotExist(); + assert.dom(".toggle-summary .top-replies").doesNotExist(); }); test("topic map - has top replies summary", async function (assert) { diff --git a/app/assets/stylesheets/common/base/_index.scss b/app/assets/stylesheets/common/base/_index.scss index 993aa21dff2..6914e39e56d 100644 --- a/app/assets/stylesheets/common/base/_index.scss +++ b/app/assets/stylesheets/common/base/_index.scss @@ -58,7 +58,6 @@ @import "tooltip"; @import "topic-admin-menu"; @import "topic-post"; -@import "topic-summary"; @import "topic"; @import "upload"; @import "user-badges"; diff --git a/app/assets/stylesheets/common/base/topic-summary.scss b/app/assets/stylesheets/common/base/topic-summary.scss deleted file mode 100644 index 162c3a5e40c..00000000000 --- a/app/assets/stylesheets/common/base/topic-summary.scss +++ /dev/null @@ -1,197 +0,0 @@ -.topic-map .toggle-summary { - .summarization-buttons { - display: flex; - gap: 0.5em; - } - - .ai-summary { - &__list { - list-style: none; - display: flex; - flex-wrap: wrap; - padding: 0; - margin: 0; - } - &__list-item { - background: var(--primary-300); - border-radius: var(--d-border-radius); - margin-right: 8px; - margin-bottom: 8px; - height: 18px; - opacity: 0; - display: block; - &:nth-child(1) { - width: 10%; - } - - &:nth-child(2) { - width: 12%; - } - - &:nth-child(3) { - width: 18%; - } - - &:nth-child(4) { - width: 14%; - } - - &:nth-child(5) { - width: 18%; - } - - &:nth-child(6) { - width: 14%; - } - - &:nth-child(7) { - width: 22%; - } - - &:nth-child(8) { - width: 05%; - } - - &:nth-child(9) { - width: 25%; - } - - &:nth-child(10) { - width: 14%; - } - - &:nth-child(11) { - width: 18%; - } - - &:nth-child(12) { - width: 12%; - } - - &:nth-child(13) { - width: 22%; - } - - &:nth-child(14) { - width: 18%; - } - - &:nth-child(15) { - width: 13%; - } - - &:nth-child(16) { - width: 22%; - } - - &:nth-child(17) { - width: 19%; - } - - &:nth-child(18) { - width: 13%; - } - - &:nth-child(19) { - width: 22%; - } - - &:nth-child(20) { - width: 25%; - } - &.is-shown { - opacity: 1; - } - &.show { - animation: appear 0.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) 0s forwards; - @media (prefers-reduced-motion) { - animation-duration: 0s; - } - } - @media (prefers-reduced-motion: no-preference) { - &.blink { - animation: blink 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) both; - } - } - } - &__generating-text { - display: inline-block; - margin-left: 3px; - } - &__indicator-wave { - flex: 0 0 auto; - display: inline-flex; - } - &__indicator-dot { - display: inline-block; - @media (prefers-reduced-motion: no-preference) { - animation: ai-summary__indicator-wave 1.8s linear infinite; - } - &:nth-child(2) { - animation-delay: -1.6s; - } - &:nth-child(3) { - animation-delay: -1.4s; - } - } - } - - .placeholder-summary { - padding-top: 0.5em; - } - - .placeholder-summary-text { - display: inline-block; - height: 1em; - margin-top: 0.6em; - width: 100%; - } - - .summarized-on { - text-align: right; - - .info-icon { - margin-left: 3px; - } - } - - .outdated-summary { - color: var(--primary-medium); - } - - .old-summary-box-temporary { - margin-top: 10px; - } -} - -@keyframes ai-summary__indicator-wave { - 0%, - 60%, - 100% { - transform: initial; - } - 30% { - transform: translateY(-0.2em); - } -} - -@keyframes appear { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -@keyframes blink { - 0% { - opacity: 1; - } - 50% { - opacity: 0.5; - } - 100% { - opacity: 1; - } -} diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 4785cfbb7a6..0a8c9fd199b 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1189,37 +1189,6 @@ class TopicsController < ApplicationController head :ok end - def summary - topic = Topic.find(params[:topic_id]) - guardian.ensure_can_see!(topic) - strategy = Summarization::Base.selected_strategy - - if strategy.nil? || !Summarization::Base.can_see_summary?(topic, current_user) - raise Discourse::NotFound - end - - RateLimiter.new(current_user, "summary", 6, 5.minutes).performed! if current_user - - opts = params.permit(:skip_age_check) - - if params[:stream] && current_user - Jobs.enqueue( - :stream_topic_summary, - topic_id: topic.id, - user_id: current_user.id, - opts: opts.as_json, - ) - - render json: success_json - else - hijack do - summary = TopicSummarization.new(strategy).summarize(topic, current_user, opts) - - render_serialized(summary, TopicSummarySerializer) - end - end - end - private def topic_params diff --git a/app/jobs/regular/stream_topic_summary.rb b/app/jobs/regular/stream_topic_summary.rb deleted file mode 100644 index a82030a613f..00000000000 --- a/app/jobs/regular/stream_topic_summary.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Jobs - class StreamTopicSummary < ::Jobs::Base - sidekiq_options retry: false - - def execute(args) - return unless topic = Topic.find_by(id: args[:topic_id]) - return unless user = User.find_by(id: args[:user_id]) - - strategy = Summarization::Base.selected_strategy - return if strategy.nil? || !Summarization::Base.can_see_summary?(topic, user) - - guardian = Guardian.new(user) - return unless guardian.can_see?(topic) - - opts = args[:opts] || {} - - streamed_summary = +"" - start = Time.now - - summary = - TopicSummarization - .new(strategy) - .summarize(topic, user, opts) do |partial_summary| - streamed_summary << partial_summary - - # Throttle updates. - if (Time.now - start > 0.5) || Rails.env.test? - payload = { done: false, topic_summary: { summarized_text: streamed_summary } } - publish_update(topic, user, payload) - start = Time.now - end - end - - publish_update( - topic, - user, - TopicSummarySerializer.new(summary, { scope: guardian }).as_json.merge(done: true), - ) - end - - private - - def publish_update(topic, user, payload) - MessageBus.publish("/summaries/topic/#{topic.id}", payload, user_ids: [user.id]) - end - end -end diff --git a/app/models/summarization_strategy.rb b/app/models/summarization_strategy.rb index cb8b6d7c0ba..dfc5c377b6b 100644 --- a/app/models/summarization_strategy.rb +++ b/app/models/summarization_strategy.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# TODO(@keegan): Remove after removing SiteSetting.summarization_strategy + require "enum_site_setting" class SummarizationStrategy < EnumSiteSetting diff --git a/app/models/summary_section.rb b/app/models/summary_section.rb deleted file mode 100644 index 21c8ab639fd..00000000000 --- a/app/models/summary_section.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -class SummarySection < ActiveRecord::Base - belongs_to :target, polymorphic: true - - def mark_as_outdated - @outdated = true - end - - def outdated - @outdated || false - end -end - -# == Schema Information -# -# Table name: summary_sections -# -# id :bigint not null, primary key -# target_id :integer not null -# target_type :string not null -# content_range :int4range -# summarized_text :string not null -# meta_section_id :integer -# original_content_sha :string not null -# algorithm :string not null -# created_at :datetime not null -# updated_at :datetime not null -# diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 4962bc0b002..fb73b850630 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -28,7 +28,6 @@ class CurrentUserSerializer < BasicUserSerializer :can_post_anonymously, :can_ignore_users, :can_delete_all_posts_and_topics, - :can_summarize, :custom_fields, :muted_category_ids, :indirectly_muted_category_ids, @@ -155,10 +154,6 @@ class CurrentUserSerializer < BasicUserSerializer object.in_any_groups?(SiteSetting.delete_all_posts_and_topics_allowed_groups_map) end - def can_summarize - object.in_any_groups?(SiteSetting.custom_summarization_allowed_groups_map) - end - def can_upload_avatar !is_anonymous && object.in_any_groups?(SiteSetting.uploaded_avatars_allowed_groups_map) end diff --git a/app/serializers/topic_summary_serializer.rb b/app/serializers/topic_summary_serializer.rb deleted file mode 100644 index 40cc0f6367a..00000000000 --- a/app/serializers/topic_summary_serializer.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -class TopicSummarySerializer < ApplicationSerializer - attributes :summarized_text, :algorithm, :outdated, :can_regenerate, :new_posts_since_summary - - def can_regenerate - Summarization::Base.can_request_summary_for?(scope.current_user) - end - - def new_posts_since_summary - # Postgres uses discrete range types for int4range, which means - # (1..2) is stored as (1...3). - # - # We use Range#max to work around this, which in the case above always returns 2. - # Be careful with using Range#end here, it could lead to unexpected results as: - # - # (1..2).end => 2 - # (1...3).end => 3 - - object.target.highest_post_number.to_i - object.content_range&.max.to_i - end -end diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index e23201281f4..0069c5fe276 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -78,7 +78,6 @@ class TopicViewSerializer < ApplicationSerializer :user_last_posted_at, :is_shared_draft, :slow_mode_enabled_until, - :summarizable, ) has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects @@ -311,10 +310,6 @@ class TopicViewSerializer < ApplicationSerializer object.topic.slow_mode_topic_timer&.execute_at end - def summarizable - object.summarizable? - end - def include_categories? scope.can_lazy_load_categories? end diff --git a/app/serializers/web_hook_topic_view_serializer.rb b/app/serializers/web_hook_topic_view_serializer.rb index 4ec6eb280e6..ff11424db15 100644 --- a/app/serializers/web_hook_topic_view_serializer.rb +++ b/app/serializers/web_hook_topic_view_serializer.rb @@ -22,7 +22,6 @@ class WebHookTopicViewSerializer < TopicViewSerializer slow_mode_seconds slow_mode_enabled_until bookmarks - summarizable ].each { |attr| define_method("include_#{attr}?") { false } } def include_show_read_indicator? diff --git a/app/services/topic_summarization.rb b/app/services/topic_summarization.rb deleted file mode 100644 index 156e6ed6dcd..00000000000 --- a/app/services/topic_summarization.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -class TopicSummarization - def initialize(strategy) - @strategy = strategy - end - - def summarize(topic, user, opts = {}, &on_partial_blk) - existing_summary = SummarySection.find_by(target: topic, meta_section_id: nil) - - # Existing summary shouldn't be nil in this scenario because the controller checks its existence. - return if !user && !existing_summary - - targets_data = summary_targets(topic).pluck(:post_number, :raw, :username) - - current_topic_sha = build_sha(targets_data.map(&:first)) - can_summarize = Summarization::Base.can_request_summary_for?(user) - - if use_cached?(existing_summary, can_summarize, current_topic_sha, !!opts[:skip_age_check]) - # It's important that we signal a cached summary is outdated - existing_summary.mark_as_outdated if new_targets?(existing_summary, current_topic_sha) - - return existing_summary - end - - delete_cached_summaries_of(topic) if existing_summary - - content = { - resource_path: "#{Discourse.base_path}/t/-/#{topic.id}", - content_title: topic.title, - contents: [], - } - - targets_data.map do |(pn, raw, username)| - raw_text = raw - - if pn == 1 && topic.topic_embed&.embed_content_cache.present? - raw_text = topic.topic_embed&.embed_content_cache - end - - content[:contents] << { poster: username, id: pn, text: raw_text } - end - - summarization_result = strategy.summarize(content, user, &on_partial_blk) - - cache_summary(summarization_result, targets_data.map(&:first), topic) - end - - def summary_targets(topic) - topic.has_summary? ? best_replies(topic) : pick_selection(topic) - end - - private - - attr_reader :strategy - - def best_replies(topic) - Post - .summary(topic.id) - .where("post_type = ?", Post.types[:regular]) - .where("NOT hidden") - .joins(:user) - .order(:post_number) - end - - def pick_selection(topic) - posts = - Post - .where(topic_id: topic.id) - .where("post_type = ?", Post.types[:regular]) - .where("NOT hidden") - .order(:post_number) - - post_numbers = posts.limit(5).pluck(:post_number) - post_numbers += posts.reorder("posts.score desc").limit(50).pluck(:post_number) - post_numbers += posts.reorder("post_number desc").limit(5).pluck(:post_number) - - Post - .where(topic_id: topic.id) - .joins(:user) - .where("post_number in (?)", post_numbers) - .order(:post_number) - end - - def delete_cached_summaries_of(topic) - SummarySection.where(target: topic).destroy_all - end - - # For users without permissions to generate a summary or fresh summaries, we return what we have cached. - def use_cached?(existing_summary, can_summarize, current_sha, skip_age_check) - existing_summary && - !( - can_summarize && new_targets?(existing_summary, current_sha) && - (skip_age_check || existing_summary.created_at < 1.hour.ago) - ) - end - - def new_targets?(summary, current_sha) - summary.original_content_sha != current_sha - end - - def cache_summary(result, post_numbers, topic) - main_summary = - SummarySection.create!( - target: topic, - algorithm: strategy.display_name, - content_range: (post_numbers.first..post_numbers.last), - summarized_text: result[:summary], - original_content_sha: build_sha(post_numbers), - ) - - result[:chunks].each do |chunk| - SummarySection.create!( - target: topic, - algorithm: strategy.display_name, - content_range: chunk[:ids].min..chunk[:ids].max, - summarized_text: chunk[:summary], - original_content_sha: build_sha(chunk[:ids]), - meta_section_id: main_summary.id, - ) - end - - main_summary - end - - def build_sha(ids) - Digest::SHA256.hexdigest(ids.join) - end -end diff --git a/config/routes.rb b/config/routes.rb index bde57811bbd..83bb6f961c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1358,11 +1358,6 @@ Discourse::Application.routes.draw do topic_id: /\d+/, } get "t/:topic_id/summary" => "topics#show", :constraints => { topic_id: /\d+/ } - get "t/:topic_id/strategy-summary" => "topics#summary", - :constraints => { - topic_id: /\d+/, - }, - :format => :json put "t/:slug/:topic_id" => "topics#update", :constraints => { topic_id: /\d+/ } put "t/:slug/:topic_id/status" => "topics#status", :constraints => { topic_id: /\d+/ } put "t/:topic_id/status" => "topics#status", :constraints => { topic_id: /\d+/ } diff --git a/config/site_settings.yml b/config/site_settings.yml index 7da9941986c..f711ef530e6 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2675,10 +2675,12 @@ uncategorized: summarization_strategy: client: true + hidden: true default: "" enum: "SummarizationStrategy" validator: "SummarizationValidator" custom_summarization_allowed_groups: + hidden: true type: group_list list_type: compact default: "3|13" # 3: @staff, 13: @trust_level_3 diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 0a0a495189c..2105a97d14a 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -1182,6 +1182,9 @@ class Plugin::Instance # to summarize content. Staff can select which strategy to use # through the `summarization_strategy` setting. def register_summarization_strategy(strategy) + Discourse.deprecate( + "register_summarization_straegy is deprecated. Summarization code is now moved to Discourse AI", + ) if !strategy.class.ancestors.include?(Summarization::Base) raise ArgumentError.new("Not a valid summarization strategy") end diff --git a/lib/summarization/base.rb b/lib/summarization/base.rb index 9c22d336c7e..24b91dc05c8 100644 --- a/lib/summarization/base.rb +++ b/lib/summarization/base.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# TODO(@keegan): Remove after removing SiteSetting.summarization_strategy +# Keeping because its still needed for SiteSetting to function. +# Remove after settings are migrated to AI + # Base class that defines the interface that every summarization # strategy must implement. # Above each method, you'll find an explanation of what diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 916bb8d2e29..fd82742d1ed 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -742,10 +742,6 @@ class TopicView end end - def summarizable? - Summarization::Base.can_see_summary?(@topic, @user) - end - def categories @categories ||= [category&.parent_category, category, suggested_topics&.categories].flatten .uniq diff --git a/lib/validators/summarization_validator.rb b/lib/validators/summarization_validator.rb index 8cb35fbb39f..e6f87e7eac0 100644 --- a/lib/validators/summarization_validator.rb +++ b/lib/validators/summarization_validator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# TODO(@keegan): Remove after removing SiteSetting.summarization_strategy + class SummarizationValidator def initialize(opts = {}) @opts = opts diff --git a/plugins/chat/app/controllers/chat/api/summaries_controller.rb b/plugins/chat/app/controllers/chat/api/summaries_controller.rb deleted file mode 100644 index 40b330bbf88..00000000000 --- a/plugins/chat/app/controllers/chat/api/summaries_controller.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -class Chat::Api::SummariesController < Chat::ApiController - VALID_SINCE_VALUES = [1, 3, 6, 12, 24, 72, 168] - - def get_summary - since = params[:since].to_i - raise Discourse::InvalidParameters.new(:since) if !VALID_SINCE_VALUES.include?(since) - - channel = Chat::Channel.find(params[:channel_id]) - guardian.ensure_can_join_chat_channel!(channel) - - strategy = Summarization::Base.selected_strategy - raise Discourse::NotFound.new unless strategy - raise Discourse::InvalidAccess unless Summarization::Base.can_request_summary_for?(current_user) - - RateLimiter.new(current_user, "channel_summary", 6, 5.minutes).performed! - - hijack do - content = { content_title: channel.name } - - content[:contents] = channel - .chat_messages - .where("chat_messages.created_at > ?", since.hours.ago) - .includes(:user) - .order(created_at: :asc) - .pluck(:id, :username_lower, :message) - .map { { id: _1, poster: _2, text: _3 } } - - summarized_text = - if content[:contents].empty? - I18n.t("chat.summaries.no_targets") - else - strategy.summarize(content, current_user).dig(:summary) - end - - render json: { summary: summarized_text } - end - end -end diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js index 0b1f54e4dac..85083251d15 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js @@ -26,7 +26,6 @@ import { import { cloneJSON } from "discourse-common/lib/object"; import { findRawTemplate } from "discourse-common/lib/raw-templates"; import I18n from "discourse-i18n"; -import ChatModalChannelSummary from "discourse/plugins/chat/discourse/components/chat/modal/channel-summary"; import { chatComposerButtons } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons"; import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor"; import TextareaInteractor from "discourse/plugins/chat/discourse/lib/textarea-interactor"; @@ -396,13 +395,6 @@ export default class ChatComposer extends Component { } } - @action - showChannelSummaryModal() { - this.modal.show(ChatModalChannelSummary, { - model: { channelId: this.args.channel.id }, - }); - } - #addMentionedUser(userData) { const user = this.store.createRecord("user", userData); this.draft.mentionedUsers.set(user.id, user); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/modal/channel-summary.gjs b/plugins/chat/assets/javascripts/discourse/components/chat/modal/channel-summary.gjs deleted file mode 100644 index 05fc163ad60..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat/modal/channel-summary.gjs +++ /dev/null @@ -1,78 +0,0 @@ -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { service } from "@ember/service"; -import ConditionalLoadingSection from "discourse/components/conditional-loading-section"; -import DModal from "discourse/components/d-modal"; -import DModalCancel from "discourse/components/d-modal-cancel"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import i18n from "discourse-common/helpers/i18n"; -import I18n from "discourse-i18n"; -import ComboBox from "select-kit/components/combo-box"; - -export default class ChatModalChannelSummary extends Component { - @service chatApi; - - @tracked sinceHours = null; - @tracked loading = false; - @tracked summary = null; - - availableSummaries = {}; - - sinceOptions = [1, 3, 6, 12, 24, 72, 168].map((hours) => { - return { - name: I18n.t("chat.summarization.since", { count: hours }), - value: hours, - }; - }); - - get channelId() { - return this.args.model.channelId; - } - - @action - summarize(since) { - this.sinceHours = since; - this.loading = true; - - if (this.availableSummaries[since]) { - this.summary = this.availableSummaries[since]; - this.loading = false; - return; - } - - return this.chatApi - .summarize(this.channelId, { since }) - .then((data) => { - this.availableSummaries[this.sinceHours] = data.summary; - this.summary = this.availableSummaries[this.sinceHours]; - }) - .catch(popupAjaxError) - .finally(() => (this.loading = false)); - } - - -} diff --git a/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-modal-channel-summary.hbs b/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-modal-channel-summary.hbs deleted file mode 100644 index 099820a0a0f..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-modal-channel-summary.hbs +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-modal-channel-summary.js b/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-modal-channel-summary.js deleted file mode 100644 index af2007f5af2..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-modal-channel-summary.js +++ /dev/null @@ -1,17 +0,0 @@ -import Component from "@glimmer/component"; -import { getOwner } from "@ember/application"; -import { action } from "@ember/object"; -import { service } from "@ember/service"; -import ChatModalChannelSummary from "discourse/plugins/chat/discourse/components/chat/modal/channel-summary"; -import ChatFabricators from "discourse/plugins/chat/discourse/lib/fabricators"; - -export default class ChatStyleguideChatModalChannelSummary extends Component { - @service modal; - - @action - openModal() { - return this.modal.show(ChatModalChannelSummary, { - model: { channelId: new ChatFabricators(getOwner(this)).channel().id }, - }); - } -} diff --git a/plugins/chat/assets/javascripts/discourse/components/styleguide/organisms/chat.gjs b/plugins/chat/assets/javascripts/discourse/components/styleguide/organisms/chat.gjs index 89640f2f43d..ec9f1dbc243 100644 --- a/plugins/chat/assets/javascripts/discourse/components/styleguide/organisms/chat.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/styleguide/organisms/chat.gjs @@ -3,7 +3,6 @@ import ChatComposerMessageDetails from "../chat-composer-message-details"; import ChatHeaderIcon from "../chat-header-icon"; import ChatMessage from "../chat-message"; import ChatModalArchiveChannel from "../chat-modal-archive-channel"; -import ChatModalChannelSummary from "../chat-modal-channel-summary"; import ChatModalCreateChannel from "../chat-modal-create-channel"; import ChatModalDeleteChannel from "../chat-modal-delete-channel"; import ChatModalEditChannelDescription from "../chat-modal-edit-channel-description"; @@ -32,7 +31,6 @@ const ChatOrganism = ; export default ChatOrganism; diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js index cb238774cf2..38a3762036d 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js @@ -97,21 +97,6 @@ export default { }, }); - const canSummarize = - this.siteSettings.summarization_strategy && - this.currentUser && - this.currentUser.can_summarize; - - if (canSummarize) { - api.registerChatComposerButton({ - translatedLabel: "chat.summarization.title", - id: "channel-summary", - icon: "discourse-sparkles", - position: "dropdown", - action: "showChannelSummaryModal", - }); - } - // we want to decorate the chat quote dates regardless // of whether the current user has chat enabled api.decorateCookedElement((elem) => { diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js index d457c92d119..95dced79542 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js @@ -578,17 +578,6 @@ export default class ChatApi extends Service { }); } - /** - * Summarize a channel. - * - * @param {number} channelId - The ID of the channel to summarize. - * @param {object} options - * @param {number} options.since - Number of hours ago the summary should start (1, 3, 6, 12, 24, 72, 168). - */ - summarize(channelId, options = {}) { - return this.#getRequest(`/channels/${channelId}/summarize`, options); - } - /** * Add members to a channel. * diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml index ac95550d96a..2550f725af7 100644 --- a/plugins/chat/config/locales/client.en.yml +++ b/plugins/chat/config/locales/client.en.yml @@ -144,13 +144,6 @@ en: join: "Join" last_visit: "last visit" - summarization: - title: "Summarize messages" - description: "Select an option below to summarize the conversation sent during the desired timeframe." - summarize: "Summarize" - since: - one: "Last hour" - other: "Last %{count} hours" mention_warning: invitations_sent: one: "Invitation sent" diff --git a/plugins/chat/config/locales/server.en.yml b/plugins/chat/config/locales/server.en.yml index 88b42c31aae..e3dc78ca341 100644 --- a/plugins/chat/config/locales/server.en.yml +++ b/plugins/chat/config/locales/server.en.yml @@ -196,9 +196,6 @@ en: one: "and %{count} other" other: "and %{count} others" - summaries: - no_targets: "There were no messages during the selected period." - transcript: default_thread_title: "Thread" split_thread_range: "messages %{start} to %{end} of %{total}" diff --git a/plugins/chat/config/routes.rb b/plugins/chat/config/routes.rb index 1e7caeacca9..d024f8a5e4e 100644 --- a/plugins/chat/config/routes.rb +++ b/plugins/chat/config/routes.rb @@ -56,8 +56,6 @@ Chat::Engine.routes.draw do put "/channels/:channel_id/messages/:message_id/restore" => "channel_messages#restore" delete "/channels/:channel_id/messages/:message_id" => "channel_messages#destroy" delete "/channels/:channel_id/messages" => "channel_messages#bulk_destroy" - - get "/channels/:channel_id/summarize" => "summaries#get_summary" end namespace :admin, defaults: { format: :json, constraints: StaffConstraint.new } do diff --git a/plugins/chat/spec/requests/chat/api/summaries_controller_spec.rb b/plugins/chat/spec/requests/chat/api/summaries_controller_spec.rb deleted file mode 100644 index 02c824b2ea8..00000000000 --- a/plugins/chat/spec/requests/chat/api/summaries_controller_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Chat::Api::SummariesController do - fab!(:current_user) { Fabricate(:user) } - fab!(:group) - let(:plugin) { Plugin::Instance.new } - - before do - group.add(current_user) - - strategy = DummyCustomSummarization.new({ summary: "dummy", chunks: [] }) - plugin.register_summarization_strategy(strategy) - SiteSetting.summarization_strategy = strategy.model - SiteSetting.custom_summarization_allowed_groups = group.id - - SiteSetting.chat_enabled = true - SiteSetting.chat_allowed_groups = group.id - sign_in(current_user) - end - - after { DiscoursePluginRegistry.reset_register!(:summarization_strategies) } - - describe "#get_summary" do - context "when the user is not allowed to join the channel" do - fab!(:channel) { Fabricate(:private_category_channel) } - - it "returns a 403" do - get "/chat/api/channels/#{channel.id}/summarize", params: { since: 6 } - - expect(response.status).to eq(403) - end - end - end -end diff --git a/plugins/chat/spec/system/chat_summarization_spec.rb b/plugins/chat/spec/system/chat_summarization_spec.rb deleted file mode 100644 index d84dda16f9c..00000000000 --- a/plugins/chat/spec/system/chat_summarization_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "Summarize a channel since your last visit", type: :system do - fab!(:current_user) { Fabricate(:user) } - fab!(:group) - fab!(:channel) { Fabricate(:chat_channel) } - fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel) } - let(:chat) { PageObjects::Pages::Chat.new } - let(:plugin) { Plugin::Instance.new } - let(:summarization_result) { { summary: "This is a summary", chunks: [] } } - - before do - group.add(current_user) - - strategy = DummyCustomSummarization.new(summarization_result) - plugin.register_summarization_strategy(strategy) - SiteSetting.summarization_strategy = strategy.model - SiteSetting.custom_summarization_allowed_groups = group.id.to_s - - SiteSetting.chat_enabled = true - SiteSetting.chat_allowed_groups = group.id.to_s - sign_in(current_user) - chat_system_bootstrap(current_user, [channel]) - end - - it "displays a summary of the messages since the selected timeframe" do - chat.visit_channel(channel) - - find(".chat-composer-dropdown__trigger-btn").click - find(".chat-composer-dropdown__action-btn.channel-summary").click - - expect(page.has_css?(".chat-modal-channel-summary")).to eq(true) - - find(".summarization-since").click - find(".select-kit-row[data-value=\"3\"]").click - - expect(find(".summary-area").text).to eq(summarization_result[:summary]) - end -end diff --git a/spec/jobs/regular/stream_topic_summary_spec.rb b/spec/jobs/regular/stream_topic_summary_spec.rb deleted file mode 100644 index d9433cf0797..00000000000 --- a/spec/jobs/regular/stream_topic_summary_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Jobs::StreamTopicSummary do - subject(:job) { described_class.new } - - describe "#execute" do - fab!(:topic) { Fabricate(:topic, highest_post_number: 2) } - fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) } - fab!(:post_2) { Fabricate(:post, topic: topic, post_number: 2) } - let(:plugin) { Plugin::Instance.new } - let(:strategy) { DummyCustomSummarization.new({ summary: "dummy", chunks: [] }) } - fab!(:user) { Fabricate(:leader) } - - before { Group.find(Group::AUTO_GROUPS[:trust_level_3]).add(user) } - - before do - plugin.register_summarization_strategy(strategy) - SiteSetting.summarization_strategy = strategy.model - end - - after { DiscoursePluginRegistry.reset_register!(:summarization_strategies) } - - describe "validates params" do - it "does nothing if there is no topic" do - messages = - MessageBus.track_publish("/summaries/topic/#{topic.id}") do - job.execute(topic_id: nil, user_id: user.id) - end - - expect(messages).to be_empty - end - - it "does nothing if there is no user" do - messages = - MessageBus.track_publish("/summaries/topic/#{topic.id}") do - job.execute(topic_id: topic.id, user_id: nil) - end - - expect(messages).to be_empty - end - - it "does nothing if the user is not allowed to see the topic" do - private_topic = Fabricate(:private_message_topic) - - messages = - MessageBus.track_publish("/summaries/topic/#{private_topic.id}") do - job.execute(topic_id: private_topic.id, user_id: user.id) - end - - expect(messages).to be_empty - end - end - - it "publishes updates with a partial summary" do - messages = - MessageBus.track_publish("/summaries/topic/#{topic.id}") do - job.execute(topic_id: topic.id, user_id: user.id) - end - - partial_summary_update = messages.first.data - expect(partial_summary_update[:done]).to eq(false) - expect(partial_summary_update.dig(:topic_summary, :summarized_text)).to eq("dummy") - end - - it "publishes a final update to signal we're done and provide metadata" do - messages = - MessageBus.track_publish("/summaries/topic/#{topic.id}") do - job.execute(topic_id: topic.id, user_id: user.id) - end - - final_update = messages.last.data - expect(final_update[:done]).to eq(true) - - expect(final_update.dig(:topic_summary, :algorithm)).to eq(strategy.model) - expect(final_update.dig(:topic_summary, :outdated)).to eq(false) - expect(final_update.dig(:topic_summary, :can_regenerate)).to eq(true) - expect(final_update.dig(:topic_summary, :new_posts_since_summary)).to be_zero - end - end -end diff --git a/spec/lib/summarization/base_spec.rb b/spec/lib/summarization/base_spec.rb deleted file mode 100644 index 2ecf8f43970..00000000000 --- a/spec/lib/summarization/base_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -describe Summarization::Base do - fab!(:user) - fab!(:group) - fab!(:topic) - - let(:plugin) { Plugin::Instance.new } - - before do - group.add(user) - - strategy = DummyCustomSummarization.new({ summary: "dummy", chunks: [] }) - plugin.register_summarization_strategy(strategy) - SiteSetting.summarization_strategy = strategy.model - end - - after { DiscoursePluginRegistry.reset_register!(:summarization_strategies) } - - describe "#can_see_summary?" do - context "when the user cannot generate a summary" do - before { SiteSetting.custom_summarization_allowed_groups = "" } - - it "returns false" do - SiteSetting.custom_summarization_allowed_groups = "" - - expect(described_class.can_see_summary?(topic, user)).to eq(false) - end - - it "returns true if there is a cached summary" do - SummarySection.create!( - target: topic, - summarized_text: "test", - original_content_sha: "123", - algorithm: "test", - meta_section_id: nil, - ) - - expect(described_class.can_see_summary?(topic, user)).to eq(true) - end - end - - context "when the user can generate a summary" do - before { SiteSetting.custom_summarization_allowed_groups = group.id } - - it "returns true if the user group is present in the custom_summarization_allowed_groups_map setting" do - expect(described_class.can_see_summary?(topic, user)).to eq(true) - end - end - - context "when there is no user" do - it "returns false for anons" do - expect(described_class.can_see_summary?(topic, nil)).to eq(false) - end - - it "returns true for anons when there is a cached summary" do - SummarySection.create!( - target: topic, - summarized_text: "test", - original_content_sha: "123", - algorithm: "test", - meta_section_id: nil, - ) - - expect(described_class.can_see_summary?(topic, nil)).to eq(true) - end - end - - context "when the topic is a PM" do - before { SiteSetting.custom_summarization_allowed_groups = group.id } - let(:pm) { Fabricate(:private_message_topic) } - - it "returns false" do - expect(described_class.can_see_summary?(pm, user)).to eq(false) - end - end - end -end diff --git a/spec/requests/api/schemas/json/topic_show_response.json b/spec/requests/api/schemas/json/topic_show_response.json index 7c728075233..1286d4a1089 100644 --- a/spec/requests/api/schemas/json/topic_show_response.json +++ b/spec/requests/api/schemas/json/topic_show_response.json @@ -7,293 +7,288 @@ "properties": { "posts": { "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "username": { - "type": "string" - }, - "avatar_template": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "cooked": { - "type": "string" - }, - "post_number": { - "type": "integer" - }, - "post_type": { - "type": "integer" - }, - "updated_at": { - "type": "string" - }, - "reply_count": { - "type": "integer" - }, - "reply_to_post_number": { - "type": [ - "string", - "null" - ] - }, - "quote_count": { - "type": "integer" - }, - "incoming_link_count": { - "type": "integer" - }, - "reads": { - "type": "integer" - }, - "readers_count": { - "type": "integer" - }, - "score": { - "type": "number" - }, - "yours": { - "type": "boolean" - }, - "topic_id": { - "type": "integer" - }, - "topic_slug": { - "type": "string" - }, - "display_username": { - "type": "string" - }, - "primary_group_name": { - "type": [ - "string", - "null" - ] - }, - "flair_name": { - "type": [ - "string", - "null" - ] - }, - "flair_url": { - "type": [ - "string", - "null" - ] - }, - "flair_bg_color": { - "type": [ - "string", - "null" - ] - }, - "flair_color": { - "type": [ - "string", - "null" - ] - }, - "version": { - "type": "integer" - }, - "can_edit": { - "type": "boolean" - }, - "can_delete": { - "type": "boolean" - }, - "can_recover": { - "type": "boolean" - }, - "can_see_hidden_post": { - "type": "boolean" - }, - "can_wiki": { - "type": "boolean" - }, - "link_counts": { - "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "string" - }, - "internal": { - "type": "boolean" - }, - "reflection": { - "type": "boolean" - }, - "title": { - "type": "string" - }, - "clicks": { - "type": "integer" - } - }, - "required": [ - "url", - "internal", - "reflection", - "title", - "clicks" - ] + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + }, + "avatar_template": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "cooked": { + "type": "string" + }, + "post_number": { + "type": "integer" + }, + "post_type": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "reply_count": { + "type": "integer" + }, + "reply_to_post_number": { + "type": [ + "string", + "null" + ] + }, + "quote_count": { + "type": "integer" + }, + "incoming_link_count": { + "type": "integer" + }, + "reads": { + "type": "integer" + }, + "readers_count": { + "type": "integer" + }, + "score": { + "type": "number" + }, + "yours": { + "type": "boolean" + }, + "topic_id": { + "type": "integer" + }, + "topic_slug": { + "type": "string" + }, + "display_username": { + "type": "string" + }, + "primary_group_name": { + "type": [ + "string", + "null" + ] + }, + "flair_name": { + "type": [ + "string", + "null" + ] + }, + "flair_url": { + "type": [ + "string", + "null" + ] + }, + "flair_bg_color": { + "type": [ + "string", + "null" + ] + }, + "flair_color": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "integer" + }, + "can_edit": { + "type": "boolean" + }, + "can_delete": { + "type": "boolean" + }, + "can_recover": { + "type": "boolean" + }, + "can_see_hidden_post": { + "type": "boolean" + }, + "can_wiki": { + "type": "boolean" + }, + "link_counts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + }, + "internal": { + "type": "boolean" + }, + "reflection": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "clicks": { + "type": "integer" } - }, - "read": { - "type": "boolean" - }, - "user_title": { - "type": [ - "string", - "null" + }, + "required": [ + "url", + "internal", + "reflection", + "title", + "clicks" ] - }, - "bookmarked": { - "type": "boolean" - }, - "actions_summary": { - "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "can_act": { - "type": "boolean" - } - }, - "required": [ - "id", - "can_act" - ] - } - }, - "moderator": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "staff": { - "type": "boolean" - }, - "user_id": { - "type": "integer" - }, - "hidden": { - "type": "boolean" - }, - "trust_level": { - "type": "integer" - }, - "deleted_at": { - "type": [ - "string", - "null" - ] - }, - "user_deleted": { - "type": "boolean" - }, - "edit_reason": { - "type": [ - "string", - "null" - ] - }, - "can_view_edit_history": { - "type": "boolean" - }, - "wiki": { - "type": "boolean" - }, - "reviewable_id": { - "type": "integer" - }, - "reviewable_score_count": { - "type": "integer" - }, - "reviewable_score_pending_count": { - "type": "integer" } }, - "required": [ - "id", - "name", - "username", - "avatar_template", - "created_at", - "cooked", - "post_number", - "post_type", - "updated_at", - "reply_count", - "reply_to_post_number", - "quote_count", - "incoming_link_count", - "reads", - "readers_count", - "score", - "yours", - "topic_id", - "topic_slug", - "display_username", - "primary_group_name", - "flair_name", - "flair_url", - "flair_bg_color", - "flair_color", - "version", - "can_edit", - "can_delete", - "can_recover", - "can_wiki", - "link_counts", - "read", - "user_title", - "bookmarked", - "actions_summary", - "moderator", - "admin", - "staff", - "user_id", - "hidden", - "trust_level", - "deleted_at", - "user_deleted", - "edit_reason", - "can_view_edit_history", - "wiki", - "reviewable_id", - "reviewable_score_count", - "reviewable_score_pending_count" - ] - } + "read": { + "type": "boolean" + }, + "user_title": { + "type": [ + "string", + "null" + ] + }, + "bookmarked": { + "type": "boolean" + }, + "actions_summary": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" + }, + "can_act": { + "type": "boolean" + } + }, + "required": [ + "id", + "can_act" + ] + } + }, + "moderator": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "staff": { + "type": "boolean" + }, + "user_id": { + "type": "integer" + }, + "hidden": { + "type": "boolean" + }, + "trust_level": { + "type": "integer" + }, + "deleted_at": { + "type": [ + "string", + "null" + ] + }, + "user_deleted": { + "type": "boolean" + }, + "edit_reason": { + "type": [ + "string", + "null" + ] + }, + "can_view_edit_history": { + "type": "boolean" + }, + "wiki": { + "type": "boolean" + }, + "reviewable_id": { + "type": "integer" + }, + "reviewable_score_count": { + "type": "integer" + }, + "reviewable_score_pending_count": { + "type": "integer" + } + }, + "required": [ + "id", + "name", + "username", + "avatar_template", + "created_at", + "cooked", + "post_number", + "post_type", + "updated_at", + "reply_count", + "reply_to_post_number", + "quote_count", + "incoming_link_count", + "reads", + "readers_count", + "score", + "yours", + "topic_id", + "topic_slug", + "display_username", + "primary_group_name", + "flair_name", + "flair_url", + "flair_bg_color", + "flair_color", + "version", + "can_edit", + "can_delete", + "can_recover", + "can_wiki", + "link_counts", + "read", + "user_title", + "bookmarked", + "actions_summary", + "moderator", + "admin", + "staff", + "user_id", + "hidden", + "trust_level", + "deleted_at", + "user_deleted", + "edit_reason", + "can_view_edit_history", + "wiki", + "reviewable_id", + "reviewable_score_count", + "reviewable_score_pending_count" + ] + } }, "stream": { "type": "array", - "items": { - - } + "items": {} } }, "required": [ @@ -303,215 +298,205 @@ }, "timeline_lookup": { "type": "array", - "items": { - - } + "items": {} }, "suggested_topics": { "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "fancy_title": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "posts_count": { - "type": "integer" - }, - "reply_count": { - "type": "integer" - }, - "highest_post_number": { - "type": "integer" - }, - "image_url": { - "type": [ - "string", - "null" - ] - }, - "created_at": { - "type": "string" - }, - "last_posted_at": { - "type": [ - "string", - "null" - ] - }, - "bumped": { - "type": "boolean" - }, - "bumped_at": { - "type": "string" - }, - "archetype": { - "type": "string" - }, - "unseen": { - "type": "boolean" - }, - "pinned": { - "type": "boolean" - }, - "unpinned": { - "type": [ - "string", - "null" - ] - }, - "excerpt": { - "type": "string" - }, - "visible": { - "type": "boolean" - }, - "closed": { - "type": "boolean" - }, - "archived": { - "type": "boolean" - }, - "bookmarked": { - "type": [ - "string", - "null" - ] - }, - "liked": { - "type": [ - "string", - "null" - ] - }, - "tags": { - "type": "array", - "items": { - - } - }, - "tags_descriptions": { + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "fancy_title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "posts_count": { + "type": "integer" + }, + "reply_count": { + "type": "integer" + }, + "highest_post_number": { + "type": "integer" + }, + "image_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string" + }, + "last_posted_at": { + "type": [ + "string", + "null" + ] + }, + "bumped": { + "type": "boolean" + }, + "bumped_at": { + "type": "string" + }, + "archetype": { + "type": "string" + }, + "unseen": { + "type": "boolean" + }, + "pinned": { + "type": "boolean" + }, + "unpinned": { + "type": [ + "string", + "null" + ] + }, + "excerpt": { + "type": "string" + }, + "visible": { + "type": "boolean" + }, + "closed": { + "type": "boolean" + }, + "archived": { + "type": "boolean" + }, + "bookmarked": { + "type": [ + "string", + "null" + ] + }, + "liked": { + "type": [ + "string", + "null" + ] + }, + "tags": { + "type": "array", + "items": {} + }, + "tags_descriptions": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "like_count": { + "type": "integer" + }, + "views": { + "type": "integer" + }, + "category_id": { + "type": "integer" + }, + "featured_link": { + "type": [ + "string", + "null" + ] + }, + "posters": { + "type": "array", + "items": { "type": "object", "additionalProperties": false, "properties": { - } - }, - "like_count": { - "type": "integer" - }, - "views": { - "type": "integer" - }, - "category_id": { - "type": "integer" - }, - "featured_link": { - "type": [ - "string", - "null" - ] - }, - "posters": { - "type": "array", - "items": - { + "extras": { + "type": "string" + }, + "description": { + "type": "string" + }, + "user": { "type": "object", "additionalProperties": false, "properties": { - "extras": { + "id": { + "type": "integer" + }, + "username": { "type": "string" }, - "description": { + "name": { "type": "string" }, - "user": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "name": { - "type": "string" - }, - "avatar_template": { - "type": "string" - } - }, - "required": [ - "id", - "username", - "name", - "avatar_template" - ] + "avatar_template": { + "type": "string" } }, "required": [ - "extras", - "description", - "user" + "id", + "username", + "name", + "avatar_template" ] } + }, + "required": [ + "extras", + "description", + "user" + ] } - }, - "required": [ - "id", - "title", - "fancy_title", - "slug", - "posts_count", - "reply_count", - "highest_post_number", - "image_url", - "created_at", - "last_posted_at", - "bumped", - "bumped_at", - "archetype", - "unseen", - "pinned", - "unpinned", - "excerpt", - "visible", - "closed", - "archived", - "bookmarked", - "liked", - "tags", - "tags_descriptions", - "like_count", - "views", - "category_id", - "featured_link", - "posters" - ] - } + } + }, + "required": [ + "id", + "title", + "fancy_title", + "slug", + "posts_count", + "reply_count", + "highest_post_number", + "image_url", + "created_at", + "last_posted_at", + "bumped", + "bumped_at", + "archetype", + "unseen", + "pinned", + "unpinned", + "excerpt", + "visible", + "closed", + "archived", + "bookmarked", + "liked", + "tags", + "tags_descriptions", + "like_count", + "views", + "category_id", + "featured_link", + "posters" + ] + } }, "tags": { "type": "array", - "items": { - - } + "items": {} }, "tags_descriptions": { "type": "object", "additionalProperties": false, - "properties": { - } + "properties": {} }, "id": { "type": "integer" @@ -650,31 +635,30 @@ }, "actions_summary": { "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "count": { - "type": "integer" - }, - "hidden": { - "type": "boolean" - }, - "can_act": { - "type": "boolean" - } + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" }, - "required": [ - "id", - "count", - "hidden", - "can_act" - ] - } + "count": { + "type": "integer" + }, + "hidden": { + "type": "boolean" + }, + "can_act": { + "type": "boolean" + } + }, + "required": [ + "id", + "count", + "hidden", + "can_act" + ] + } }, "chunk_size": { "type": "integer" @@ -684,9 +668,7 @@ }, "bookmarks": { "type": "array", - "items": { - - } + "items": {} }, "topic_timer": { "type": [ @@ -715,9 +697,6 @@ "null" ] }, - "summarizable": { - "type": "boolean" - }, "details": { "type": "object", "additionalProperties": false, @@ -784,88 +763,87 @@ }, "participants": { "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "name": { - "type": "string" - }, - "avatar_template": { - "type": "string" - }, - "post_count": { - "type": "integer" - }, - "primary_group_name": { - "type": [ - "string", - "null" - ] - }, - "flair_name": { - "type": [ - "string", - "null" - ] - }, - "flair_url": { - "type": [ - "string", - "null" - ] - }, - "flair_color": { - "type": [ - "string", - "null" - ] - }, - "flair_bg_color": { - "type": [ - "string", - "null" - ] - }, - "flair_group_id": { - "type": [ - "integer", - "null" - ] - }, - "admin": { - "type": "boolean" - }, - "moderator": { - "type": "boolean" - }, - "trust_level": { - "type": "integer" - } + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" }, - "required": [ - "id", - "username", - "name", - "avatar_template", - "post_count", - "primary_group_name", - "flair_name", - "flair_url", - "flair_color", - "flair_bg_color", - "admin", - "moderator", - "trust_level" - ] - } + "username": { + "type": "string" + }, + "name": { + "type": "string" + }, + "avatar_template": { + "type": "string" + }, + "post_count": { + "type": "integer" + }, + "primary_group_name": { + "type": [ + "string", + "null" + ] + }, + "flair_name": { + "type": [ + "string", + "null" + ] + }, + "flair_url": { + "type": [ + "string", + "null" + ] + }, + "flair_color": { + "type": [ + "string", + "null" + ] + }, + "flair_bg_color": { + "type": [ + "string", + "null" + ] + }, + "flair_group_id": { + "type": [ + "integer", + "null" + ] + }, + "admin": { + "type": "boolean" + }, + "moderator": { + "type": "boolean" + }, + "trust_level": { + "type": "integer" + } + }, + "required": [ + "id", + "username", + "name", + "avatar_template", + "post_count", + "primary_group_name", + "flair_name", + "flair_url", + "flair_color", + "flair_bg_color", + "admin", + "moderator", + "trust_level" + ] + } }, "created_by": { "type": "object", @@ -988,7 +966,6 @@ "show_read_indicator", "thumbnails", "slow_mode_enabled_until", - "summarizable", "details" ] -} +} \ No newline at end of file diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index c06ed29b7c6..95ee3fa3df6 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -5703,126 +5703,4 @@ RSpec.describe TopicsController do end end end - - describe "#summary" do - fab!(:topic) { Fabricate(:topic, highest_post_number: 2) } - fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) } - fab!(:post_2) { Fabricate(:post, topic: topic, post_number: 2) } - let(:plugin) { Plugin::Instance.new } - let(:strategy) { DummyCustomSummarization.new({ summary: "dummy", chunks: [] }) } - - before do - plugin.register_summarization_strategy(strategy) - SiteSetting.summarization_strategy = strategy.model - end - - after { DiscoursePluginRegistry.reset_register!(:summarization_strategies) } - - context "for anons" do - it "returns a 404 if there is no cached summary" do - get "/t/#{topic.id}/strategy-summary.json" - - expect(response.status).to eq(404) - end - - it "returns a cached summary" do - section = - SummarySection.create!( - target: topic, - summarized_text: "test", - algorithm: "test", - original_content_sha: "test", - ) - - get "/t/#{topic.id}/strategy-summary.json" - - expect(response.status).to eq(200) - - summary = response.parsed_body - expect(summary.dig("topic_summary", "summarized_text")).to eq(section.summarized_text) - end - end - - context "when the user is a member of an allowlisted group" do - fab!(:user) { Fabricate(:leader) } - - before do - sign_in(user) - Group.find(Group::AUTO_GROUPS[:trust_level_3]).add(user) - end - - it "returns a 404 if there is no topic" do - invalid_topic_id = 999 - - get "/t/#{invalid_topic_id}/strategy-summary.json" - - expect(response.status).to eq(404) - end - - it "returns a 403 if not allowed to see the topic" do - pm = Fabricate(:private_message_topic) - - get "/t/#{pm.id}/strategy-summary.json" - - expect(response.status).to eq(403) - end - - it "returns a summary" do - get "/t/#{topic.id}/strategy-summary.json" - - expect(response.status).to eq(200) - summary = response.parsed_body["topic_summary"] - section = SummarySection.last - - expect(summary["summarized_text"]).to eq(section.summarized_text) - expect(summary["algorithm"]).to eq(strategy.model) - expect(summary["outdated"]).to eq(false) - expect(summary["can_regenerate"]).to eq(true) - expect(summary["new_posts_since_summary"]).to be_zero - end - - it "signals the summary is outdated" do - get "/t/#{topic.id}/strategy-summary.json" - - Fabricate(:post, topic: topic, post_number: 3) - topic.update!(highest_post_number: 3) - - get "/t/#{topic.id}/strategy-summary.json" - expect(response.status).to eq(200) - summary = response.parsed_body["topic_summary"] - - expect(summary["outdated"]).to eq(true) - expect(summary["new_posts_since_summary"]).to eq(1) - end - end - - context "when the user is not a member of an allowlisted group" do - fab!(:user) - - before { sign_in(user) } - - it "return a 404 if there is no cached summary" do - get "/t/#{topic.id}/strategy-summary.json" - - expect(response.status).to eq(404) - end - - it "returns a cached summary" do - section = - SummarySection.create!( - target: topic, - summarized_text: "test", - algorithm: "test", - original_content_sha: "test", - ) - - get "/t/#{topic.id}/strategy-summary.json" - - expect(response.status).to eq(200) - - summary = response.parsed_body - expect(summary.dig("topic_summary", "summarized_text")).to eq(section.summarized_text) - end - end - end end diff --git a/spec/services/topic_summarization_spec.rb b/spec/services/topic_summarization_spec.rb deleted file mode 100644 index 7a282d2506a..00000000000 --- a/spec/services/topic_summarization_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -describe TopicSummarization do - fab!(:user) { Fabricate(:admin) } - fab!(:topic) { Fabricate(:topic, highest_post_number: 2) } - fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) } - fab!(:post_2) { Fabricate(:post, topic: topic, post_number: 2) } - - shared_examples "includes only public-visible topics" do - subject { described_class.new(DummyCustomSummarization.new({})) } - - it "only includes visible posts" do - topic.first_post.update!(hidden: true) - - posts = subject.summary_targets(topic) - - expect(posts.none?(&:hidden?)).to eq(true) - end - - it "doesn't include posts without users" do - topic.first_post.user.destroy! - - posts = subject.summary_targets(topic) - - expect(posts.detect { |p| p.id == topic.first_post.id }).to be_nil - end - - it "doesn't include deleted posts" do - topic.first_post.update!(user_id: nil) - - posts = subject.summary_targets(topic) - - expect(posts.detect { |p| p.id == topic.first_post.id }).to be_nil - end - end - - describe "#summary_targets" do - context "when the topic has a best replies summary" do - before { topic.has_summary = true } - - it_behaves_like "includes only public-visible topics" - end - - context "when the topic doesn't have a best replies summary" do - before { topic.has_summary = false } - - it_behaves_like "includes only public-visible topics" - end - end - - describe "#summarize" do - subject(:summarization) { described_class.new(strategy) } - - let(:strategy) { DummyCustomSummarization.new(summary) } - - def assert_summary_is_cached(topic, summary_response) - cached_summary = SummarySection.find_by(target: topic, meta_section_id: nil) - - expect(cached_summary.content_range).to cover(*topic.posts.map(&:post_number)) - expect(cached_summary.summarized_text).to eq(summary_response[:summary]) - expect(cached_summary.original_content_sha).to be_present - expect(cached_summary.algorithm).to eq(strategy.model) - end - - def assert_chunk_is_cached(topic, chunk_response) - cached_chunk = - SummarySection - .where.not(meta_section_id: nil) - .find_by( - target: topic, - content_range: (chunk_response[:ids].min..chunk_response[:ids].max), - ) - - expect(cached_chunk.summarized_text).to eq(chunk_response[:summary]) - expect(cached_chunk.original_content_sha).to be_present - expect(cached_chunk.algorithm).to eq(strategy.model) - end - - context "when the content was summarized in a single chunk" do - let(:summary) { { summary: "This is the final summary", chunks: [] } } - - it "caches the summary" do - section = summarization.summarize(topic, user) - - expect(section.summarized_text).to eq(summary[:summary]) - - assert_summary_is_cached(topic, summary) - end - - it "returns the cached version in subsequent calls" do - summarization.summarize(topic, user) - - cached_summary_text = "This is a cached summary" - cached_summary = - SummarySection.find_by(target: topic, meta_section_id: nil).update!( - summarized_text: cached_summary_text, - updated_at: 24.hours.ago, - ) - - section = summarization.summarize(topic, user) - expect(section.summarized_text).to eq(cached_summary_text) - end - - context "when the topic has embed content cached" do - it "embed content is used instead of the raw text" do - topic_embed = - Fabricate( - :topic_embed, - topic: topic, - embed_content_cache: "

hello world new post :D

", - ) - - summarization.summarize(topic, user) - - first_post_data = - strategy.content[:contents].detect { |c| c[:id] == topic.first_post.post_number } - - expect(first_post_data[:text]).to eq(topic_embed.embed_content_cache) - end - end - end - - context "when the content was summarized in multiple chunks" do - let(:summary) do - { - summary: "This is the final summary", - chunks: [ - { ids: [topic.first_post.post_number], summary: "this is the first chunk" }, - { ids: [post_1.post_number, post_2.post_number], summary: "this is the second chunk" }, - ], - } - end - - it "caches the summary and each chunk" do - section = summarization.summarize(topic, user) - - expect(section.summarized_text).to eq(summary[:summary]) - - assert_summary_is_cached(topic, summary) - - summary[:chunks].each { |c| assert_chunk_is_cached(topic, c) } - end - end - - describe "invalidating cached summaries" do - let(:cached_text) { "This is a cached summary" } - let(:summarized_text) { "This is the final summary" } - let(:summary) do - { - summary: summarized_text, - chunks: [ - { ids: [topic.first_post.post_number], summary: "this is the first chunk" }, - { ids: [post_1.post_number, post_2.post_number], summary: "this is the second chunk" }, - ], - } - end - - def cached_summary - SummarySection.find_by(target: topic, meta_section_id: nil) - end - - before do - summarization.summarize(topic, user) - - cached_summary.update!(summarized_text: cached_text, created_at: 24.hours.ago) - end - - context "when the user can requests new summaries" do - context "when there are no new posts" do - it "returns the cached summary" do - section = summarization.summarize(topic, user) - - expect(section.summarized_text).to eq(cached_text) - end - end - - context "when there are new posts" do - before { cached_summary.update!(original_content_sha: "outdated_sha") } - - it "returns a new summary" do - section = summarization.summarize(topic, user) - - expect(section.summarized_text).to eq(summarized_text) - end - - context "when the cached summary is less than one hour old" do - before { cached_summary.update!(created_at: 30.minutes.ago) } - - it "returns the cached summary" do - cached_summary.update!(created_at: 30.minutes.ago) - - section = summarization.summarize(topic, user) - - expect(section.summarized_text).to eq(cached_text) - expect(section.outdated).to eq(true) - end - - it "returns a new summary if the skip_age_check flag is passed" do - section = summarization.summarize(topic, user, skip_age_check: true) - - expect(section.summarized_text).to eq(summarized_text) - end - end - end - end - end - - describe "stream partial updates" do - let(:summary) { { summary: "This is the final summary", chunks: [] } } - - it "receives a blk that is passed to the underlying strategy and called with partial summaries" do - partial_result = nil - - summarization.summarize(topic, user) { |partial_summary| partial_result = partial_summary } - - expect(partial_result).to eq(summary[:summary]) - end - end - end -end diff --git a/spec/support/dummy_custom_summarization.rb b/spec/support/dummy_custom_summarization.rb deleted file mode 100644 index 45fd1166524..00000000000 --- a/spec/support/dummy_custom_summarization.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class DummyCustomSummarization < Summarization::Base - def initialize(summarization_result) - @summarization_result = summarization_result - end - - def display_name - "dummy" - end - - def correctly_configured? - true - end - - def configuration_hint - "hint" - end - - def model - "dummy" - end - - def summarize(content, _user) - @content = content - @summarization_result.tap { |result| yield(result[:summary]) if block_given? } - end - - attr_reader :content -end