UX: move topic summary from DMenu to DModal (#992)

Co-authored-by: Keegan George <kgeorge13@gmail.com>
This commit is contained in:
Kris 2024-12-03 13:30:15 -05:00 committed by GitHub
parent ce6a2eca21
commit 8203bdfbc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 171 additions and 178 deletions

View File

@ -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,50 +204,25 @@ 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"
{{didInsert this.subscribe @model.topic.id}}
{{didUpdate this.subscribe @model.topic.id}}
{{willDestroy this.unsubscribe}} {{willDestroy this.unsubscribe}}
@hideFooter={{not this.summarizedOn}}
> >
<DMenu <:body>
@onShow={{this.generateSummary}} {{htmlClass "scrollable-modal"}}
@arrow={{false}} <div class="ai-summary-container" {{didInsert this.generateSummary}}>
@identifier="topic-map__ai-summary"
@onRegisterApi={{this.onRegisterApi}}
@interactive={{true}}
@triggers="click"
@placement="left"
@modalForMobile={{true}}
@groupIdentifier="topic-map"
@inline={{true}}
@label={{i18n "summary.buttons.generate"}}
@title={{i18n "summary.buttons.generate"}}
@icon="discourse-sparkles"
@triggerClass="ai-topic-summarization"
@closeOnClickOutside={{false}}
>
<:content>
<div class="ai-summary-container">
<header class="ai-summary__header">
<h3>{{i18n "discourse_ai.summarization.topic.title"}}</h3>
{{#if this.site.desktopView}}
<DButton
@title="discourse_ai.summarization.topic.close"
@action={{this.onClose}}
@icon="times"
class="btn-transparent ai-summary__close"
/>
{{/if}}
</header>
<article <article
class={{concatClass class={{concatClass
"ai-summary-box" "ai-summary-box"
@ -263,25 +236,24 @@ export default class AiSummaryBox extends Component {
<div class="generated-summary cooked"> <div class="generated-summary cooked">
<CookText @rawText={{this.streamedText}} /> <CookText @rawText={{this.streamedText}} />
</div> </div>
{{#if this.summarizedOn}} {{/if}}
<div class="summarized-on"> </article>
<p> </div>
</:body>
<:footer>
<p class="summarized-on">
{{i18n "summary.summarized_on" date=this.summarizedOn}} {{i18n "summary.summarized_on" date=this.summarizedOn}}
<DTooltip @placements={{array "top-end"}}> <DTooltip @placements={{array "top-end"}}>
<:trigger> <:trigger>
{{dIcon "info-circle"}} {{dIcon "circle-info"}}
</:trigger> </:trigger>
<:content> <:content>
{{i18n {{i18n "summary.model_used" model=this.summarizedBy}}
"summary.model_used"
model=this.summarizedBy
}}
</:content> </:content>
</DTooltip> </DTooltip>
</p> </p>
<div class="outdated-summary">
{{#if this.outdated}} {{#if this.outdated}}
<p>{{this.outdatedSummaryWarningText}}</p> <p class="summary-outdated">{{this.outdatedSummaryWarningText}}</p>
{{/if}} {{/if}}
{{#if this.canRegenerate}} {{#if this.canRegenerate}}
<DButton <DButton
@ -291,15 +263,7 @@ export default class AiSummaryBox extends Component {
@icon="sync" @icon="sync"
/> />
{{/if}} {{/if}}
</div> </:footer>
</div> </DModal>
{{/if}}
{{/if}}
</article>
</div>
</:content>
</DMenu>
</div>
{{/if}}
</template> </template>
} }

View File

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

View File

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

View File

@ -1,39 +1,27 @@
.topic-map { html.scrollable-modal {
.ai-summarization-button { overflow: auto; // overrides core .modal-open class scroll lock
.fk-d-menu { }
.ai-summary-modal {
.d-modal__container {
position: fixed; position: fixed;
top: calc(var(--header-offset) + 1rem) !important; top: var(--header-offset);
left: unset !important; margin-top: 1em;
right: 1rem !important; right: 1em;
width: 470px; width: 100vw;
max-width: 470px !important; //overruling JS max-width: 30em;
max-height: calc( max-height: calc(
100vh - var(--header-offset) - 3rem - var(--composer-height, 0px) 100vh - var(--header-offset) - 3rem - var(--composer-height, 0px)
); );
.ai-summary__header, box-shadow: var(--shadow-menu-panel);
.ai-summary-box {
padding: 0.75em 1rem;
box-sizing: border-box;
} }
.ai-summary { .fullscreen-composer & {
&__header { display: none;
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 {
margin: 0;
}
}
}
}
} }
} }
.ai-summary-modal + .d-modal__backdrop {
display: none;
}

View File

@ -1,3 +0,0 @@
.ai-summary-box {
padding: 0.75em 1rem;
}

View File

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

View File

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

View File

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

View File

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