UX: move topic summary from DMenu to DModal (#992)
Co-authored-by: Keegan George <kgeorge13@gmail.com>
This commit is contained in:
parent
ce6a2eca21
commit
8203bdfbc9
|
@ -7,26 +7,29 @@ import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||||
import { cancel, later } from "@ember/runloop";
|
import { cancel, later } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
import { not } from "truth-helpers";
|
||||||
import CookText from "discourse/components/cook-text";
|
import CookText from "discourse/components/cook-text";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DModal from "discourse/components/d-modal";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import htmlClass from "discourse/helpers/html-class";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { shortDateNoYear } from "discourse/lib/formatter";
|
import { shortDateNoYear } from "discourse/lib/formatter";
|
||||||
import dIcon from "discourse-common/helpers/d-icon";
|
import dIcon from "discourse-common/helpers/d-icon";
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
import DMenu from "float-kit/components/d-menu";
|
|
||||||
import DTooltip from "float-kit/components/d-tooltip";
|
import DTooltip from "float-kit/components/d-tooltip";
|
||||||
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
|
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
|
||||||
|
|
||||||
const STREAMED_TEXT_SPEED = 15;
|
const STREAMED_TEXT_SPEED = 15;
|
||||||
|
|
||||||
export default class AiSummaryBox extends Component {
|
export default class AiSummaryModal extends Component {
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@service messageBus;
|
@service messageBus;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service site;
|
@service site;
|
||||||
|
@service modal;
|
||||||
|
|
||||||
@tracked text = "";
|
@tracked text = "";
|
||||||
@tracked summarizedOn = null;
|
@tracked summarizedOn = null;
|
||||||
|
@ -68,11 +71,11 @@ export default class AiSummaryBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get topRepliesSummaryEnabled() {
|
get topRepliesSummaryEnabled() {
|
||||||
return this.args.outletArgs.postStream.summary;
|
return this.args.model.postStream.summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
get topicId() {
|
get topicId() {
|
||||||
return this.args.outletArgs.topic.id;
|
return this.args.model.topic.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseSummarizationURL() {
|
get baseSummarizationURL() {
|
||||||
|
@ -80,13 +83,8 @@ export default class AiSummaryBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
subscribe(unsubscribe, [topicId]) {
|
subscribe() {
|
||||||
const sameTopicId = this.args.outletArgs.topic.id === topicId;
|
const channel = `/discourse-ai/summaries/topic/${this.args.model.topic.id}`;
|
||||||
|
|
||||||
if (unsubscribe && this._channel && !sameTopicId) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
|
|
||||||
this._channel = channel;
|
this._channel = channel;
|
||||||
this.messageBus.subscribe(channel, this._updateSummary);
|
this.messageBus.subscribe(channel, this._updateSummary);
|
||||||
}
|
}
|
||||||
|
@ -206,100 +204,66 @@ export default class AiSummaryBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async onClose() {
|
handleClose() {
|
||||||
await this.dMenu.close();
|
this.modal.triggerElement = null; // prevent refocus of trigger, which changes scroll position
|
||||||
this.unsubscribe();
|
this.args.closeModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if @outletArgs.topic.summarizable}}
|
<DModal
|
||||||
<div
|
@title={{i18n "discourse_ai.summarization.topic.title"}}
|
||||||
class="ai-summarization-button"
|
@closeModal={{this.handleClose}}
|
||||||
{{didInsert this.subscribe}}
|
@bodyClass="ai-summary-modal__body"
|
||||||
{{didUpdate this.subscribe @outletArgs.topic.id}}
|
class="ai-summary-modal"
|
||||||
{{willDestroy this.unsubscribe}}
|
{{didInsert this.subscribe @model.topic.id}}
|
||||||
>
|
{{didUpdate this.subscribe @model.topic.id}}
|
||||||
<DMenu
|
{{willDestroy this.unsubscribe}}
|
||||||
@onShow={{this.generateSummary}}
|
@hideFooter={{not this.summarizedOn}}
|
||||||
@arrow={{false}}
|
>
|
||||||
@identifier="topic-map__ai-summary"
|
<:body>
|
||||||
@onRegisterApi={{this.onRegisterApi}}
|
{{htmlClass "scrollable-modal"}}
|
||||||
@interactive={{true}}
|
<div class="ai-summary-container" {{didInsert this.generateSummary}}>
|
||||||
@triggers="click"
|
<article
|
||||||
@placement="left"
|
class={{concatClass
|
||||||
@modalForMobile={{true}}
|
"ai-summary-box"
|
||||||
@groupIdentifier="topic-map"
|
"streamable-content"
|
||||||
@inline={{true}}
|
(if this.isStreaming "streaming")
|
||||||
@label={{i18n "summary.buttons.generate"}}
|
}}
|
||||||
@title={{i18n "summary.buttons.generate"}}
|
>
|
||||||
@icon="discourse-sparkles"
|
{{#if this.loading}}
|
||||||
@triggerClass="ai-topic-summarization"
|
<AiSummarySkeleton />
|
||||||
@closeOnClickOutside={{false}}
|
{{else}}
|
||||||
>
|
<div class="generated-summary cooked">
|
||||||
<:content>
|
<CookText @rawText={{this.streamedText}} />
|
||||||
<div class="ai-summary-container">
|
</div>
|
||||||
<header class="ai-summary__header">
|
{{/if}}
|
||||||
<h3>{{i18n "discourse_ai.summarization.topic.title"}}</h3>
|
</article>
|
||||||
{{#if this.site.desktopView}}
|
</div>
|
||||||
<DButton
|
</:body>
|
||||||
@title="discourse_ai.summarization.topic.close"
|
<:footer>
|
||||||
@action={{this.onClose}}
|
<p class="summarized-on">
|
||||||
@icon="times"
|
{{i18n "summary.summarized_on" date=this.summarizedOn}}
|
||||||
class="btn-transparent ai-summary__close"
|
<DTooltip @placements={{array "top-end"}}>
|
||||||
/>
|
<:trigger>
|
||||||
{{/if}}
|
{{dIcon "circle-info"}}
|
||||||
</header>
|
</:trigger>
|
||||||
|
<:content>
|
||||||
<article
|
{{i18n "summary.model_used" model=this.summarizedBy}}
|
||||||
class={{concatClass
|
</:content>
|
||||||
"ai-summary-box"
|
</DTooltip>
|
||||||
"streamable-content"
|
</p>
|
||||||
(if this.isStreaming "streaming")
|
{{#if this.outdated}}
|
||||||
}}
|
<p class="summary-outdated">{{this.outdatedSummaryWarningText}}</p>
|
||||||
>
|
{{/if}}
|
||||||
{{#if this.loading}}
|
{{#if this.canRegenerate}}
|
||||||
<AiSummarySkeleton />
|
<DButton
|
||||||
{{else}}
|
@label="summary.buttons.regenerate"
|
||||||
<div class="generated-summary cooked">
|
@title="summary.buttons.regenerate"
|
||||||
<CookText @rawText={{this.streamedText}} />
|
@action={{this.regenerateSummary}}
|
||||||
</div>
|
@icon="sync"
|
||||||
{{#if this.summarizedOn}}
|
/>
|
||||||
<div class="summarized-on">
|
{{/if}}
|
||||||
<p>
|
</:footer>
|
||||||
{{i18n "summary.summarized_on" date=this.summarizedOn}}
|
</DModal>
|
||||||
<DTooltip @placements={{array "top-end"}}>
|
|
||||||
<:trigger>
|
|
||||||
{{dIcon "info-circle"}}
|
|
||||||
</:trigger>
|
|
||||||
<:content>
|
|
||||||
{{i18n
|
|
||||||
"summary.model_used"
|
|
||||||
model=this.summarizedBy
|
|
||||||
}}
|
|
||||||
</:content>
|
|
||||||
</DTooltip>
|
|
||||||
</p>
|
|
||||||
<div class="outdated-summary">
|
|
||||||
{{#if this.outdated}}
|
|
||||||
<p>{{this.outdatedSummaryWarningText}}</p>
|
|
||||||
{{/if}}
|
|
||||||
{{#if this.canRegenerate}}
|
|
||||||
<DButton
|
|
||||||
@label="summary.buttons.regenerate"
|
|
||||||
@title="summary.buttons.regenerate"
|
|
||||||
@action={{this.regenerateSummary}}
|
|
||||||
@icon="sync"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</:content>
|
|
||||||
</DMenu>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import AiSummaryModal from "../../components/modal/ai-summary-modal";
|
||||||
|
|
||||||
|
export default class AiSummaryTrigger extends Component {
|
||||||
|
@service modal;
|
||||||
|
|
||||||
|
@action
|
||||||
|
openAiSummaryModal() {
|
||||||
|
this.modal.show(AiSummaryModal, {
|
||||||
|
model: {
|
||||||
|
topic: this.args.outletArgs.topic,
|
||||||
|
postStream: this.args.outletArgs.postStream,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if @outletArgs.topic.summarizable}}
|
||||||
|
<DButton
|
||||||
|
@label="summary.buttons.generate"
|
||||||
|
@icon="discourse-sparkles"
|
||||||
|
@action={{this.openAiSummaryModal}}
|
||||||
|
class="ai-summarization-button"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -24,25 +24,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-map__additional-contents {
|
|
||||||
.ai-summarization-button {
|
|
||||||
padding-block: 0.5em;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
button span {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-map__ai-summary-content {
|
.ai-summary-modal {
|
||||||
.ai-summary-container {
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-summary {
|
.ai-summary {
|
||||||
&__list {
|
&__list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@ -174,14 +158,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summarized-on p {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.25em;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.outdated-summary {
|
.outdated-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -193,6 +169,40 @@
|
||||||
color: var(--primary-medium);
|
color: var(--primary-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.d-modal__footer {
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
grid-template-areas: "summarized regenerate" " outdated regenerate";
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
@include breakpoint(mobile-large) {
|
||||||
|
gap: 0.25em 0.5em;
|
||||||
|
grid-template-areas: "summarized summarized" "regenerate outdated";
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fk-d-tooltip__trigger {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-outdated {
|
||||||
|
color: var(--primary-high);
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
line-height: var(--line-height-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summarized-on {
|
||||||
|
grid-area: summarized;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
grid-area: regenerate;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
|
|
|
@ -1,39 +1,27 @@
|
||||||
.topic-map {
|
html.scrollable-modal {
|
||||||
.ai-summarization-button {
|
overflow: auto; // overrides core .modal-open class scroll lock
|
||||||
.fk-d-menu {
|
}
|
||||||
position: fixed;
|
|
||||||
top: calc(var(--header-offset) + 1rem) !important;
|
|
||||||
left: unset !important;
|
|
||||||
right: 1rem !important;
|
|
||||||
width: 470px;
|
|
||||||
max-width: 470px !important; //overruling JS
|
|
||||||
max-height: calc(
|
|
||||||
100vh - var(--header-offset) - 3rem - var(--composer-height, 0px)
|
|
||||||
);
|
|
||||||
|
|
||||||
.ai-summary__header,
|
.ai-summary-modal {
|
||||||
.ai-summary-box {
|
.d-modal__container {
|
||||||
padding: 0.75em 1rem;
|
position: fixed;
|
||||||
box-sizing: border-box;
|
top: var(--header-offset);
|
||||||
}
|
margin-top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 30em;
|
||||||
|
max-height: calc(
|
||||||
|
100vh - var(--header-offset) - 3rem - var(--composer-height, 0px)
|
||||||
|
);
|
||||||
|
|
||||||
.ai-summary {
|
box-shadow: var(--shadow-menu-panel);
|
||||||
&__header {
|
}
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-block: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
background: var(--secondary);
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
|
|
||||||
h3 {
|
.fullscreen-composer & {
|
||||||
margin: 0;
|
display: none;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-summary-modal + .d-modal__backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
.ai-summary-box {
|
|
||||||
padding: 0.75em 1rem;
|
|
||||||
}
|
|
|
@ -20,7 +20,6 @@ register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss"
|
||||||
register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop
|
register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop
|
||||||
register_asset "stylesheets/modules/ai-helper/mobile/ai-helper.scss", :mobile
|
register_asset "stylesheets/modules/ai-helper/mobile/ai-helper.scss", :mobile
|
||||||
|
|
||||||
register_asset "stylesheets/modules/summarization/mobile/ai-summary.scss", :mobile
|
|
||||||
register_asset "stylesheets/modules/summarization/common/ai-summary.scss"
|
register_asset "stylesheets/modules/summarization/common/ai-summary.scss"
|
||||||
register_asset "stylesheets/modules/summarization/desktop/ai-summary.scss", :desktop
|
register_asset "stylesheets/modules/summarization/desktop/ai-summary.scss", :desktop
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
module PageObjects
|
module PageObjects
|
||||||
module Components
|
module Components
|
||||||
class AiSummaryBox < PageObjects::Components::Base
|
class AiSummaryTrigger < PageObjects::Components::Base
|
||||||
SUMMARY_BUTTON_SELECTOR = ".ai-summarization-button button"
|
SUMMARY_BUTTON_SELECTOR = ".ai-summarization-button"
|
||||||
SUMMARY_CONTAINER_SELECTOR = ".ai-summary-container"
|
SUMMARY_CONTAINER_SELECTOR = ".ai-summary-modal"
|
||||||
|
|
||||||
def click_summarize
|
def click_summarize
|
||||||
find(SUMMARY_BUTTON_SELECTOR).click
|
find(SUMMARY_BUTTON_SELECTOR).click
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_regenerate_summary
|
def click_regenerate_summary
|
||||||
find("#{SUMMARY_CONTAINER_SELECTOR} .outdated-summary button").click
|
find("#{SUMMARY_CONTAINER_SELECTOR} .d-modal__footer button").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_summary?(summary)
|
def has_summary?(summary)
|
||||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe "Summarize a topic ", type: :system do
|
||||||
end
|
end
|
||||||
let(:summarization_result) { "This is a summary" }
|
let(:summarization_result) { "This is a summary" }
|
||||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||||
let(:summary_box) { PageObjects::Components::AiSummaryBox.new }
|
let(:summary_box) { PageObjects::Components::AiSummaryTrigger.new }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
group.add(current_user)
|
group.add(current_user)
|
||||||
|
|
|
@ -22,7 +22,12 @@ acceptance("Topic - Summary", function (needs) {
|
||||||
});
|
});
|
||||||
|
|
||||||
server.get("/discourse-ai/summarization/t/1", () => {
|
server.get("/discourse-ai/summarization/t/1", () => {
|
||||||
return helper.response({});
|
return helper.response({
|
||||||
|
ai_topic_summary: {
|
||||||
|
summarized_text: "This a",
|
||||||
|
},
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,7 +44,7 @@ acceptance("Topic - Summary", function (needs) {
|
||||||
ai_topic_summary: { summarized_text: partialSummary },
|
ai_topic_summary: { summarized_text: partialSummary },
|
||||||
});
|
});
|
||||||
|
|
||||||
await click(".ai-topic-summarization");
|
await click(".ai-summarization-button");
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(".ai-summary-box .generated-summary p")
|
.dom(".ai-summary-box .generated-summary p")
|
||||||
|
@ -63,7 +68,7 @@ acceptance("Topic - Summary", function (needs) {
|
||||||
.hasText(finalSummary, "Updates the summary with a final result");
|
.hasText(finalSummary, "Updates the summary with a final result");
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(".ai-summary-box .summarized-on")
|
.dom(".ai-summary-modal .summarized-on")
|
||||||
.exists("summary metadata exists");
|
.exists("summary metadata exists");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -76,7 +81,7 @@ acceptance("Topic - Summary", function (needs) {
|
||||||
ai_topic_summary: { summarized_text: partialSummary },
|
ai_topic_summary: { summarized_text: partialSummary },
|
||||||
});
|
});
|
||||||
|
|
||||||
await click(".ai-topic-summarization");
|
await click(".ai-summarization-button");
|
||||||
const finalSummaryCooked =
|
const finalSummaryCooked =
|
||||||
"In this post, <a href='/t/-/1/1'>bianca</a> said some stuff.";
|
"In this post, <a href='/t/-/1/1'>bianca</a> said some stuff.";
|
||||||
const finalSummaryResult = "In this post, bianca said some stuff.";
|
const finalSummaryResult = "In this post, bianca said some stuff.";
|
||||||
|
@ -128,20 +133,20 @@ acceptance("Topic - Summary - Anon", function (needs) {
|
||||||
test("displays cached summary immediately", async function (assert) {
|
test("displays cached summary immediately", async function (assert) {
|
||||||
await visit("/t/-/1");
|
await visit("/t/-/1");
|
||||||
|
|
||||||
await click(".ai-topic-summarization");
|
await click(".ai-summarization-button");
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(".ai-summary-box .generated-summary p")
|
.dom(".ai-summary-box .generated-summary p")
|
||||||
.hasText(finalSummary, "Updates the summary with the result");
|
.hasText(finalSummary, "Updates the summary with the result");
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(".ai-summary-box .summarized-on")
|
.dom(".ai-summary-modal .summarized-on")
|
||||||
.exists("summary metadata exists");
|
.exists("summary metadata exists");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("clicking outside of summary should not close the summary box", async function (assert) {
|
test("clicking outside of summary should not close the summary box", async function (assert) {
|
||||||
await visit("/t/-/1");
|
await visit("/t/-/1");
|
||||||
await click(".ai-topic-summarization");
|
await click(".ai-summarization-button");
|
||||||
await click("#main-outlet-wrapper");
|
await click("#main-outlet-wrapper");
|
||||||
assert.dom(".ai-summary-box").exists();
|
assert.dom(".ai-summary-box").exists();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue