From 0f1479e8962675530f9a8a332a0c90c7d63b68e6 Mon Sep 17 00:00:00 2001
From: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com>
Date: Fri, 28 Jul 2023 14:02:26 -0500
Subject: [PATCH] UX: Refactor AI summarizing animation (#22839)
---
.../app/components/ai-summary-skeleton.hbs | 26 +++
.../app/components/ai-summary-skeleton.js | 92 +++++++++
.../discourse/app/widgets/summary-box.js | 43 +----
.../common/base/topic-summary.scss | 179 +++++++++++++++---
4 files changed, 278 insertions(+), 62 deletions(-)
create mode 100644 app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs
create mode 100644 app/assets/javascripts/discourse/app/components/ai-summary-skeleton.js
diff --git a/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs
new file mode 100644
index 00000000000..410f6376e0b
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.hbs
@@ -0,0 +1,26 @@
+
+ {{#each this.blocks as |block|}}
+
+ {{/each}}
+
+
+
+
+ {{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
new file mode 100644
index 00000000000..1323a7b3bf9
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/ai-summary-skeleton.js
@@ -0,0 +1,92 @@
+import Component from "@glimmer/component";
+import { action } from "@ember/object";
+import { tracked } from "@glimmer/tracking";
+import discourseLater from "discourse-common/lib/later";
+import { cancel } from "@ember/runloop";
+
+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/widgets/summary-box.js b/app/assets/javascripts/discourse/app/widgets/summary-box.js
index 01f3b8c5774..fa0e425a1b6 100644
--- a/app/assets/javascripts/discourse/app/widgets/summary-box.js
+++ b/app/assets/javascripts/discourse/app/widgets/summary-box.js
@@ -10,39 +10,6 @@ import { h } from "virtual-dom";
import { iconNode } from "discourse-common/lib/icon-library";
import RenderGlimmer from "discourse/widgets/render-glimmer";
-createWidget("summary-skeleton", {
- tagName: "section.placeholder-summary",
-
- html() {
- const html = [];
-
- html.push(this.buildPlaceholderDiv());
- html.push(this.buildPlaceholderDiv());
- html.push(this.buildPlaceholderDiv());
-
- html.push(
- h("span", {}, [
- h(
- "div.placeholder-generating-summary-text",
- {},
- I18n.t("summary.in_progress")
- ),
- h("span.ai-summarizing-indicator__wave", {}, [
- h("span.ai-summarizing-indicator__dot", "."),
- h("span.ai-summarizing-indicator__dot", "."),
- h("span.ai-summarizing-indicator__dot", "."),
- ]),
- ])
- );
-
- return html;
- },
-
- buildPlaceholderDiv() {
- return h("div.placeholder-summary-text.placeholder-animation");
- },
-});
-
export default createWidget("summary-box", {
tagName: "article.summary-box",
buildKey: (attrs) => `summary-box-${attrs.topicId}`,
@@ -76,13 +43,21 @@ export default createWidget("summary-box", {
html.push(h("div.summarized-on", {}, summarizationInfo));
} else {
- html.push(this.attach("summary-skeleton"));
+ html.push(this.buildSummarySkeleton());
this.fetchSummary(attrs.topicId, attrs.skipAgeCheck);
}
return html;
},
+ buildSummarySkeleton() {
+ return new RenderGlimmer(
+ this,
+ "div.ai-summary__container",
+ hbs`{{ai-summary-skeleton}}`
+ );
+ },
+
buildTooltip(attrs) {
return new RenderGlimmer(
this,
diff --git a/app/assets/stylesheets/common/base/topic-summary.scss b/app/assets/stylesheets/common/base/topic-summary.scss
index 4c9d1dbcfeb..781f8ddc37b 100644
--- a/app/assets/stylesheets/common/base/topic-summary.scss
+++ b/app/assets/stylesheets/common/base/topic-summary.scss
@@ -1,33 +1,124 @@
-.topic-map {
- .toggle-summary {
- .summarization-buttons {
+.topic-map .toggle-summary {
+ .summarization-buttons {
+ display: flex;
+ }
+
+ .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%;
+ }
- .placeholder-summary {
- padding-top: 0.5em;
+ &: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;
+ }
+ &.blink {
+ animation: blink 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
+ }
}
-
- .placeholder-summary-text {
- display: inline-block;
- height: 1em;
- margin-top: 0.6em;
- width: 100%;
- }
-
- .placeholder-generating-summary-text {
+ &__generating-text {
display: inline-block;
margin-left: 3px;
}
-
- .ai-summarizing-indicator__wave {
+ &__indicator-wave {
flex: 0 0 auto;
display: inline-flex;
}
-
- .ai-summarizing-indicator__dot {
+ &__indicator-dot {
display: inline-block;
- animation: ai-summarizing-indicator__wave 1.8s linear infinite;
+ animation: ai-summary__indicator-wave 1.8s linear infinite;
&:nth-child(2) {
animation-delay: -1.6s;
}
@@ -35,22 +126,33 @@
animation-delay: -1.4s;
}
}
+ }
- .summarized-on {
- text-align: right;
+ .placeholder-summary {
+ padding-top: 0.5em;
+ }
- .info-icon {
- margin-left: 3px;
- }
+ .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);
- }
+ .outdated-summary {
+ color: var(--primary-medium);
}
}
-@keyframes ai-summarizing-indicator__wave {
+@keyframes ai-summary__indicator-wave {
0%,
60%,
100% {
@@ -60,3 +162,24 @@
transform: translateY(-0.2em);
}
}
+
+@keyframes appear {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+}