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.
This commit is contained in:
Sam 2024-01-09 23:20:28 +11:00 committed by GitHub
parent b0a0cbe3ca
commit 05f7808057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 85 additions and 24 deletions

View File

@ -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
);
}
},

View File

@ -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);

View File

@ -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"

View File

@ -152,10 +152,17 @@ module DiscourseAi
<summary>#{summary}</summary>
<p>#{details}</p>
</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 << "<span></span>\n\n"
end
placeholder
end

View File

@ -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: "<p></p>")
redis_stream_key = "gpt_cancel:#{reply_post.id}"
Discourse.redis.setex(redis_stream_key, 60, 1)

View File

@ -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)

View File

@ -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

View File

@ -45,6 +45,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
<summary>#{tool.summary}</summary>
<p></p>
</details>
<span></span>
HTML

View File

@ -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("<p></p>")
messages[1..-1].each_with_index do |m, idx|
expect(m.data[:raw]).to eq(expected_bot_response[0..idx])
end