From 05f7808057183bbdb05be9eb4fd3ba0b58baac9a Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Jan 2024 23:20:28 +1100 Subject: [PATCH] FEATURE: more elegant progress (#409) Previous to this change it was very hard to tell if completion was stuck or not. This introduces a "dot" that follows the completion and starts flashing after 5 seconds. --- .../initializers/ai-bot-replies.js | 38 +++++++++++++++++-- .../modules/ai-bot/common/bot-replies.scss | 29 ++++++++++++++ config/locales/server.en.yml | 4 +- lib/ai_bot/bot.rb | 11 +++++- lib/ai_bot/playground.rb | 4 +- lib/ai_bot/tools/dall_e.rb | 8 +--- lib/ai_bot/tools/image.rb | 10 ++--- spec/lib/modules/ai_bot/bot_spec.rb | 1 + spec/lib/modules/ai_bot/playground_spec.rb | 4 +- 9 files changed, 85 insertions(+), 24 deletions(-) diff --git a/assets/javascripts/initializers/ai-bot-replies.js b/assets/javascripts/initializers/ai-bot-replies.js index ff6551d1..774fdf37 100644 --- a/assets/javascripts/initializers/ai-bot-replies.js +++ b/assets/javascripts/initializers/ai-bot-replies.js @@ -1,3 +1,4 @@ +import { later } from "@ember/runloop"; import { hbs } from "ember-cli-htmlbars"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -94,15 +95,41 @@ function initializeAIBotReplies(api) { onAIBotStreamedReply: function (data) { const post = this.model.postStream.findLoadedPost(data.post_id); + // it may take us a few seconds to load the post + // we need to requeue the event + if (!post && !data.done) { + const refresh = this.onAIBotStreamedReply.bind(this); + data.retries = data.retries || 5; + data.retries -= 1; + data.skipIfStreaming = true; + if (data.retries > 0) { + later(() => { + refresh(data); + }, 1000); + } + } + if (post) { if (data.raw) { + const postElement = document.querySelector( + `#post_${data.post_number}` + ); + + if ( + data.skipIfStreaming && + postElement.classList.contains("streaming") + ) { + return; + } + cook(data.raw).then((cooked) => { post.set("raw", data.raw); post.set("cooked", cooked); - document - .querySelector(`#post_${data.post_number}`) - .classList.add("streaming"); + // resets animation + postElement.classList.remove("streaming"); + void postElement.offsetWidth; + postElement.classList.add("streaming"); const cookedElement = document.createElement("div"); cookedElement.innerHTML = cooked; @@ -131,9 +158,12 @@ function initializeAIBotReplies(api) { this.model.details.allowed_users && this.model.details.allowed_users.filter(isGPTBot).length >= 1 ) { + // we attempt to recover the last message in the bus + // so we subscribe at -2 this.messageBus.subscribe( `discourse-ai/ai-bot/topic/${this.model.id}`, - this.onAIBotStreamedReply.bind(this) + this.onAIBotStreamedReply.bind(this), + -2 ); } }, diff --git a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss index 2a8ba805..eeefb0ea 100644 --- a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss +++ b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss @@ -47,6 +47,35 @@ article.streaming nav.post-controls .actions button.cancel-streaming { display: inline-block; } +@keyframes flashing { + 0%, + 100% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +article.streaming .cooked > { + :not(ol):not(ul):not(pre):last-child::after, + ol:last-child li:last-child p:last-child::after, + ol:last-child li:last-child:not(:has(p))::after, + ul:last-child li:last-child p:last-child::after, + ul:last-child li:last-child:not(:has(p))::after, + pre:last-child code::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; + } +} + .ai-bot-available-bot-options { .ai-bot-available-bot-content { color: var(--primary-high); diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ce4d3aec..9d3dffd6 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -130,13 +130,11 @@ en: explain: "Explain" illustrate_post: "Illustrate Post" painter: - attribution: + attribution: stable_diffusion_xl: "Image by Stable Diffusion XL" dall_e_3: "Image by DALL-E 3" ai_bot: - placeholder_reply: "I will reply shortly..." - personas: cannot_delete_system_persona: "System personas cannot be deleted, please disable it instead" cannot_edit_system_persona: "System personas can only be renamed, you may not edit commands or system prompt, instead disable and make a copy" diff --git a/lib/ai_bot/bot.rb b/lib/ai_bot/bot.rb index c67880e5..661ee8d4 100644 --- a/lib/ai_bot/bot.rb +++ b/lib/ai_bot/bot.rb @@ -152,10 +152,17 @@ module DiscourseAi #{summary}

#{details}

- HTML - placeholder << custom_raw if custom_raw + if custom_raw + placeholder << "\n" + placeholder << custom_raw + else + # we need this for cursor placeholder to work + # doing this in CSS is very hard + # if changing test with a custom tool such as search + placeholder << "\n\n" + end placeholder end diff --git a/lib/ai_bot/playground.rb b/lib/ai_bot/playground.rb index 293c0fbe..2bb6940a 100644 --- a/lib/ai_bot/playground.rb +++ b/lib/ai_bot/playground.rb @@ -107,10 +107,12 @@ module DiscourseAi PostCreator.create!( bot.bot_user, topic_id: post.topic_id, - raw: I18n.t("discourse_ai.ai_bot.placeholder_reply"), + raw: "", skip_validations: true, ) + publish_update(reply_post, raw: "

") + redis_stream_key = "gpt_cancel:#{reply_post.id}" Discourse.redis.setex(redis_stream_key, 60, 1) diff --git a/lib/ai_bot/tools/dall_e.rb b/lib/ai_bot/tools/dall_e.rb index 85621056..96548f6b 100644 --- a/lib/ai_bot/tools/dall_e.rb +++ b/lib/ai_bot/tools/dall_e.rb @@ -36,7 +36,7 @@ module DiscourseAi def invoke(bot_user, _llm) # max 4 prompts max_prompts = prompts.take(4) - progress = +"" + progress = prompts.first yield(progress) @@ -70,11 +70,7 @@ module DiscourseAi end end - while true - progress << "." - yield(progress) - break if threads.all? { |t| t.join(2) } - end + break if threads.all? { |t| t.join(2) } while true results = threads.filter_map(&:value) diff --git a/lib/ai_bot/tools/image.rb b/lib/ai_bot/tools/image.rb index e9d47fa3..92f3a3d4 100644 --- a/lib/ai_bot/tools/image.rb +++ b/lib/ai_bot/tools/image.rb @@ -51,7 +51,7 @@ module DiscourseAi selected_prompts = prompts.take(4) seeds = seeds.take(4) if seeds - progress = +"" + progress = prompts.first yield(progress) results = nil @@ -85,11 +85,7 @@ module DiscourseAi end end - while true - progress << "." - yield(progress) - break if threads.all? { |t| t.join(2) } - end + break if threads.all? { |t| t.join(2) } while true results = threads.map(&:value).compact @@ -115,7 +111,7 @@ module DiscourseAi end @custom_raw = <<~RAW - + [grid] #{ uploads diff --git a/spec/lib/modules/ai_bot/bot_spec.rb b/spec/lib/modules/ai_bot/bot_spec.rb index a5ac0185..b3ab4506 100644 --- a/spec/lib/modules/ai_bot/bot_spec.rb +++ b/spec/lib/modules/ai_bot/bot_spec.rb @@ -45,6 +45,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do #{tool.summary}

+ HTML diff --git a/spec/lib/modules/ai_bot/playground_spec.rb b/spec/lib/modules/ai_bot/playground_spec.rb index b9a05297..a938db1b 100644 --- a/spec/lib/modules/ai_bot/playground_spec.rb +++ b/spec/lib/modules/ai_bot/playground_spec.rb @@ -67,7 +67,9 @@ RSpec.describe DiscourseAi::AiBot::Playground do done_signal = messages.pop expect(done_signal.data[:done]).to eq(true) - messages.each_with_index do |m, idx| + # we need this for styling + expect(messages.first.data[:raw]).to eq("

") + messages[1..-1].each_with_index do |m, idx| expect(m.data[:raw]).to eq(expected_bot_response[0..idx]) end