diff --git a/app/jobs/regular/stream_topic_ai_summary.rb b/app/jobs/regular/stream_topic_ai_summary.rb index d3212f58..c9756801 100644 --- a/app/jobs/regular/stream_topic_ai_summary.rb +++ b/app/jobs/regular/stream_topic_ai_summary.rb @@ -26,7 +26,7 @@ module Jobs streamed_summary << partial_summary # Throttle updates. - if (Time.now - start > 0.3) || Rails.env.test? + if (Time.now - start > 0.5) || Rails.env.test? payload = { done: false, ai_topic_summary: { summarized_text: streamed_summary } } publish_update(topic, user, payload) diff --git a/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs b/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs index 65619092..40e944ba 100644 --- a/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs +++ b/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs @@ -9,6 +9,7 @@ import { service } from "@ember/service"; import DButton from "discourse/components/d-button"; import { ajax } from "discourse/lib/ajax"; import { shortDateNoYear } from "discourse/lib/formatter"; +import { cook } from "discourse/lib/text"; import dIcon from "discourse-common/helpers/d-icon"; import i18n from "discourse-common/helpers/i18n"; import { bind } from "discourse-common/utils/decorators"; @@ -16,7 +17,6 @@ import I18n from "discourse-i18n"; import DMenu from "float-kit/components/d-menu"; import DTooltip from "float-kit/components/d-tooltip"; import AiSummarySkeleton from "../../components/ai-summary-skeleton"; -import { streamSummaryText } from "../../lib/ai-streamer"; export default class AiSummaryBox extends Component { @service siteSettings; @@ -31,8 +31,6 @@ export default class AiSummaryBox extends Component { @tracked outdated = false; @tracked canRegenerate = false; @tracked loading = false; - oldRaw = null; // used for comparison in SummaryUpdater in lib/ai-streamer - finalSummary = null; get outdatedSummaryWarningText() { let outdatedText = I18n.t("summary.outdated"); @@ -79,8 +77,7 @@ export default class AiSummaryBox extends Component { } const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`; this._channel = channel; - // we attempt to recover the last message in the bus so we subscrcibe at -2 - this.messageBus.subscribe(channel, this._updateSummary, -2); + this.messageBus.subscribe(channel, this._updateSummary); } @bind @@ -137,26 +134,26 @@ export default class AiSummaryBox extends Component { @bind _updateSummary(update) { - const topicSummary = { - done: update.done, - raw: update.ai_topic_summary?.summarized_text, - ...update.ai_topic_summary, - }; - this.loading = false; + const topicSummary = update.ai_topic_summary; - streamSummaryText(topicSummary, this); - - if (update.done) { - this.text = this.finalSummary; - this.summarizedOn = shortDateNoYear( - moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z") - ); - this.summarizedBy = topicSummary.algorithm; - this.newPostsSinceSummary = topicSummary.new_posts_since_summary; - this.outdated = topicSummary.outdated; - this.newPostsSinceSummary = topicSummary.new_posts_since_summary; - this.canRegenerate = topicSummary.outdated && topicSummary.can_regenerate; - } + return cook(topicSummary.summarized_text) + .then((cooked) => { + this.text = cooked; + this.loading = false; + }) + .then(() => { + if (update.done) { + this.summarizedOn = shortDateNoYear( + moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z") + ); + this.summarizedBy = topicSummary.algorithm; + this.newPostsSinceSummary = topicSummary.new_posts_since_summary; + this.outdated = topicSummary.outdated; + this.newPostsSinceSummary = topicSummary.new_posts_since_summary; + this.canRegenerate = + topicSummary.outdated && topicSummary.can_regenerate; + } + }); } @action @@ -212,7 +209,7 @@ export default class AiSummaryBox extends Component { {{#if this.loading}} {{else}} -
{{this.text}}
+
{{this.text}}
{{#if this.summarizedOn}}

diff --git a/assets/javascripts/discourse/lib/ai-streamer.js b/assets/javascripts/discourse/lib/ai-streamer.js index 494788c5..d6d06af6 100644 --- a/assets/javascripts/discourse/lib/ai-streamer.js +++ b/assets/javascripts/discourse/lib/ai-streamer.js @@ -134,63 +134,6 @@ class PostUpdater extends StreamUpdater { } } -export class SummaryUpdater extends StreamUpdater { - constructor(topicSummary, componentContext) { - super(); - this.topicSummary = topicSummary; - this.componentContext = componentContext; - - if (this.topicSummary) { - this.summaryBox = document.querySelector("article.ai-summary-box"); - } - } - - get element() { - return this.summaryBox; - } - - set streaming(value) { - if (this.element) { - if (value) { - this.element.classList.add("streaming"); - } else { - this.element.classList.remove("streaming"); - } - } - } - - async setRaw(value, done) { - this.componentContext.oldRaw = value; - const cooked = await cook(value); - - // resets animation - this.element.classList.remove("streaming"); - void this.element.offsetWidth; - this.element.classList.add("streaming"); - - const cookedElement = document.createElement("div"); - cookedElement.innerHTML = cooked; - - if (!done) { - addProgressDot(cookedElement); - } - await this.setCooked(cookedElement.innerHTML); - - if (done) { - this.componentContext.finalSummary = cooked; - } - } - - async setCooked(value) { - const cookedContainer = this.element.querySelector(".generated-summary"); - cookedContainer.innerHTML = value; - } - - get raw() { - return this.componentContext.oldRaw || ""; - } -} - export async function applyProgress(status, updater) { status.startTime = status.startTime || Date.now(); @@ -205,6 +148,7 @@ export async function applyProgress(status, updater) { } const oldRaw = updater.raw; + if (status.raw === oldRaw && !status.done) { const hasProgressDot = updater.element.querySelector(".progress-dot"); if (hasProgressDot) { @@ -269,22 +213,6 @@ async function handleProgress(postStream) { return keepPolling; } -export function streamSummaryText(topicSummary, context) { - const summaryUpdater = new SummaryUpdater(topicSummary, context); - - if (!progressTimer) { - progressTimer = later(async () => { - await applyProgress(topicSummary, summaryUpdater); - - progressTimer = null; - - if (!topicSummary.done) { - await applyProgress(topicSummary, summaryUpdater); - } - }, PROGRESS_INTERVAL); - } -} - function ensureProgress(postStream) { if (!progressTimer) { progressTimer = later(async () => { diff --git a/assets/stylesheets/common/streaming.scss b/assets/stylesheets/common/streaming.scss deleted file mode 100644 index 9608749a..00000000 --- a/assets/stylesheets/common/streaming.scss +++ /dev/null @@ -1,31 +0,0 @@ -@keyframes flashing { - 0%, - 100% { - opacity: 0; - } - 50% { - opacity: 1; - } -} - -article.streaming .cooked { - .progress-dot::after { - content: "\25CF"; - font-family: Söhne Circle, system-ui, -apple-system, Segoe UI, Roboto, - Ubuntu, Cantarell, Noto Sans, sans-serif; - line-height: normal; - margin-left: 0.25rem; - vertical-align: baseline; - animation: flashing 1.5s 3s infinite; - display: inline-block; - font-size: 1rem; - color: var(--tertiary-medium); - } - - > .progress-dot:only-child::after { - // if the progress dot is the only content - // we are likely waiting longer for a response - // so it can start animating instantly - animation: flashing 1.5s infinite; - } -} diff --git a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss index e2a2518a..aa404fe3 100644 --- a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss +++ b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss @@ -51,6 +51,38 @@ article.streaming nav.post-controls .actions button.cancel-streaming { display: inline-block; } +@keyframes flashing { + 0%, + 100% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +article.streaming .cooked { + .progress-dot::after { + content: "\25CF"; + font-family: Söhne Circle, system-ui, -apple-system, Segoe UI, Roboto, + Ubuntu, Cantarell, Noto Sans, sans-serif; + line-height: normal; + margin-left: 0.25rem; + vertical-align: baseline; + animation: flashing 1.5s 3s infinite; + display: inline-block; + font-size: 1rem; + color: var(--tertiary-medium); + } + + > .progress-dot:only-child::after { + // if the progress dot is the only content + // we are likely waiting longer for a response + // so it can start animating instantly + animation: flashing 1.5s infinite; + } +} + .ai-bot-available-bot-options { padding: 0.5em; diff --git a/plugin.rb b/plugin.rb index a221f9db..501a0304 100644 --- a/plugin.rb +++ b/plugin.rb @@ -13,8 +13,6 @@ gem "tiktoken_ruby", "0.0.9" enabled_site_setting :discourse_ai_enabled -register_asset "stylesheets/common/streaming.scss" - register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss" register_asset "stylesheets/modules/ai-helper/mobile/ai-helper.scss", :mobile diff --git a/test/javascripts/acceptance/topic-summary-test.js b/test/javascripts/acceptance/topic-summary-test.js index 8a915b95..b051471a 100644 --- a/test/javascripts/acceptance/topic-summary-test.js +++ b/test/javascripts/acceptance/topic-summary-test.js @@ -1,5 +1,5 @@ import { click, visit } from "@ember/test-helpers"; -import { skip } from "qunit"; +import { test } from "qunit"; import topicFixtures from "discourse/tests/fixtures/topic"; import { acceptance, @@ -30,7 +30,7 @@ acceptance("Topic - Summary", function (needs) { updateCurrentUser({ id: currentUserId }); }); - skip("displays streamed summary", async function (assert) { + test("displays streamed summary", async function (assert) { await visit("/t/-/1"); const partialSummary = "This a"; @@ -67,7 +67,7 @@ acceptance("Topic - Summary", function (needs) { .exists("summary metadata exists"); }); - skip("clicking summary links", async function (assert) { + test("clicking summary links", async function (assert) { await visit("/t/-/1"); const partialSummary = "In this post,"; @@ -125,7 +125,7 @@ acceptance("Topic - Summary - Anon", function (needs) { }); }); - skip("displays cached summary immediately", async function (assert) { + test("displays cached summary immediately", async function (assert) { await visit("/t/-/1"); await click(".ai-topic-summarization");