Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 8203bdf

Browse files
UX: move topic summary from DMenu to DModal (#992)
Co-authored-by: Keegan George <[email protected]>
1 parent ce6a2ec commit 8203bdf

File tree

9 files changed

+171
-178
lines changed

9 files changed

+171
-178
lines changed
Lines changed: 67 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,29 @@ import didUpdate from "@ember/render-modifiers/modifiers/did-update";
77
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
88
import { cancel, later } from "@ember/runloop";
99
import { service } from "@ember/service";
10+
import { not } from "truth-helpers";
1011
import CookText from "discourse/components/cook-text";
1112
import DButton from "discourse/components/d-button";
13+
import DModal from "discourse/components/d-modal";
1214
import concatClass from "discourse/helpers/concat-class";
15+
import htmlClass from "discourse/helpers/html-class";
1316
import { ajax } from "discourse/lib/ajax";
1417
import { shortDateNoYear } from "discourse/lib/formatter";
1518
import dIcon from "discourse-common/helpers/d-icon";
1619
import i18n from "discourse-common/helpers/i18n";
1720
import { bind } from "discourse-common/utils/decorators";
1821
import I18n from "discourse-i18n";
19-
import DMenu from "float-kit/components/d-menu";
2022
import DTooltip from "float-kit/components/d-tooltip";
2123
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
2224

2325
const STREAMED_TEXT_SPEED = 15;
2426

25-
export default class AiSummaryBox extends Component {
27+
export default class AiSummaryModal extends Component {
2628
@service siteSettings;
2729
@service messageBus;
2830
@service currentUser;
2931
@service site;
32+
@service modal;
3033

3134
@tracked text = "";
3235
@tracked summarizedOn = null;
@@ -68,25 +71,20 @@ export default class AiSummaryBox extends Component {
6871
}
6972

7073
get topRepliesSummaryEnabled() {
71-
return this.args.outletArgs.postStream.summary;
74+
return this.args.model.postStream.summary;
7275
}
7376

7477
get topicId() {
75-
return this.args.outletArgs.topic.id;
78+
return this.args.model.topic.id;
7679
}
7780

7881
get baseSummarizationURL() {
7982
return `/discourse-ai/summarization/t/${this.topicId}`;
8083
}
8184

8285
@bind
83-
subscribe(unsubscribe, [topicId]) {
84-
const sameTopicId = this.args.outletArgs.topic.id === topicId;
85-
86-
if (unsubscribe && this._channel && !sameTopicId) {
87-
this.unsubscribe();
88-
}
89-
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
86+
subscribe() {
87+
const channel = `/discourse-ai/summaries/topic/${this.args.model.topic.id}`;
9088
this._channel = channel;
9189
this.messageBus.subscribe(channel, this._updateSummary);
9290
}
@@ -206,100 +204,66 @@ export default class AiSummaryBox extends Component {
206204
}
207205

208206
@action
209-
async onClose() {
210-
await this.dMenu.close();
211-
this.unsubscribe();
207+
handleClose() {
208+
this.modal.triggerElement = null; // prevent refocus of trigger, which changes scroll position
209+
this.args.closeModal();
212210
}
213211

214212
<template>
215-
{{#if @outletArgs.topic.summarizable}}
216-
<div
217-
class="ai-summarization-button"
218-
{{didInsert this.subscribe}}
219-
{{didUpdate this.subscribe @outletArgs.topic.id}}
220-
{{willDestroy this.unsubscribe}}
221-
>
222-
<DMenu
223-
@onShow={{this.generateSummary}}
224-
@arrow={{false}}
225-
@identifier="topic-map__ai-summary"
226-
@onRegisterApi={{this.onRegisterApi}}
227-
@interactive={{true}}
228-
@triggers="click"
229-
@placement="left"
230-
@modalForMobile={{true}}
231-
@groupIdentifier="topic-map"
232-
@inline={{true}}
233-
@label={{i18n "summary.buttons.generate"}}
234-
@title={{i18n "summary.buttons.generate"}}
235-
@icon="discourse-sparkles"
236-
@triggerClass="ai-topic-summarization"
237-
@closeOnClickOutside={{false}}
238-
>
239-
<:content>
240-
<div class="ai-summary-container">
241-
<header class="ai-summary__header">
242-
<h3>{{i18n "discourse_ai.summarization.topic.title"}}</h3>
243-
{{#if this.site.desktopView}}
244-
<DButton
245-
@title="discourse_ai.summarization.topic.close"
246-
@action={{this.onClose}}
247-
@icon="times"
248-
class="btn-transparent ai-summary__close"
249-
/>
250-
{{/if}}
251-
</header>
252-
253-
<article
254-
class={{concatClass
255-
"ai-summary-box"
256-
"streamable-content"
257-
(if this.isStreaming "streaming")
258-
}}
259-
>
260-
{{#if this.loading}}
261-
<AiSummarySkeleton />
262-
{{else}}
263-
<div class="generated-summary cooked">
264-
<CookText @rawText={{this.streamedText}} />
265-
</div>
266-
{{#if this.summarizedOn}}
267-
<div class="summarized-on">
268-
<p>
269-
{{i18n "summary.summarized_on" date=this.summarizedOn}}
270-
<DTooltip @placements={{array "top-end"}}>
271-
<:trigger>
272-
{{dIcon "info-circle"}}
273-
</:trigger>
274-
<:content>
275-
{{i18n
276-
"summary.model_used"
277-
model=this.summarizedBy
278-
}}
279-
</:content>
280-
</DTooltip>
281-
</p>
282-
<div class="outdated-summary">
283-
{{#if this.outdated}}
284-
<p>{{this.outdatedSummaryWarningText}}</p>
285-
{{/if}}
286-
{{#if this.canRegenerate}}
287-
<DButton
288-
@label="summary.buttons.regenerate"
289-
@title="summary.buttons.regenerate"
290-
@action={{this.regenerateSummary}}
291-
@icon="sync"
292-
/>
293-
{{/if}}
294-
</div>
295-
</div>
296-
{{/if}}
297-
{{/if}}
298-
</article>
299-
</div>
300-
</:content>
301-
</DMenu>
302-
</div>
303-
{{/if}}
213+
<DModal
214+
@title={{i18n "discourse_ai.summarization.topic.title"}}
215+
@closeModal={{this.handleClose}}
216+
@bodyClass="ai-summary-modal__body"
217+
class="ai-summary-modal"
218+
{{didInsert this.subscribe @model.topic.id}}
219+
{{didUpdate this.subscribe @model.topic.id}}
220+
{{willDestroy this.unsubscribe}}
221+
@hideFooter={{not this.summarizedOn}}
222+
>
223+
<:body>
224+
{{htmlClass "scrollable-modal"}}
225+
<div class="ai-summary-container" {{didInsert this.generateSummary}}>
226+
<article
227+
class={{concatClass
228+
"ai-summary-box"
229+
"streamable-content"
230+
(if this.isStreaming "streaming")
231+
}}
232+
>
233+
{{#if this.loading}}
234+
<AiSummarySkeleton />
235+
{{else}}
236+
<div class="generated-summary cooked">
237+
<CookText @rawText={{this.streamedText}} />
238+
</div>
239+
{{/if}}
240+
</article>
241+
</div>
242+
</:body>
243+
<:footer>
244+
<p class="summarized-on">
245+
{{i18n "summary.summarized_on" date=this.summarizedOn}}
246+
<DTooltip @placements={{array "top-end"}}>
247+
<:trigger>
248+
{{dIcon "circle-info"}}
249+
</:trigger>
250+
<:content>
251+
{{i18n "summary.model_used" model=this.summarizedBy}}
252+
</:content>
253+
</DTooltip>
254+
</p>
255+
{{#if this.outdated}}
256+
<p class="summary-outdated">{{this.outdatedSummaryWarningText}}</p>
257+
{{/if}}
258+
{{#if this.canRegenerate}}
259+
<DButton
260+
@label="summary.buttons.regenerate"
261+
@title="summary.buttons.regenerate"
262+
@action={{this.regenerateSummary}}
263+
@icon="sync"
264+
/>
265+
{{/if}}
266+
</:footer>
267+
</DModal>
304268
</template>
305269
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
3+
import { service } from "@ember/service";
4+
import DButton from "discourse/components/d-button";
5+
import AiSummaryModal from "../../components/modal/ai-summary-modal";
6+
7+
export default class AiSummaryTrigger extends Component {
8+
@service modal;
9+
10+
@action
11+
openAiSummaryModal() {
12+
this.modal.show(AiSummaryModal, {
13+
model: {
14+
topic: this.args.outletArgs.topic,
15+
postStream: this.args.outletArgs.postStream,
16+
},
17+
});
18+
}
19+
20+
<template>
21+
{{#if @outletArgs.topic.summarizable}}
22+
<DButton
23+
@label="summary.buttons.generate"
24+
@icon="discourse-sparkles"
25+
@action={{this.openAiSummaryModal}}
26+
class="ai-summarization-button"
27+
/>
28+
{{/if}}
29+
</template>
30+
}

assets/stylesheets/modules/summarization/common/ai-summary.scss

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,9 @@
2424
}
2525
}
2626
}
27-
28-
.topic-map__additional-contents {
29-
.ai-summarization-button {
30-
padding-block: 0.5em;
31-
display: flex;
32-
gap: 0.5em;
33-
34-
button span {
35-
white-space: nowrap;
36-
}
37-
}
38-
}
3927
}
4028

41-
.topic-map__ai-summary-content {
42-
.ai-summary-container {
43-
width: 100vw;
44-
}
45-
29+
.ai-summary-modal {
4630
.ai-summary {
4731
&__list {
4832
list-style: none;
@@ -174,14 +158,6 @@
174158
margin: 0;
175159
}
176160

177-
.summarized-on p {
178-
display: flex;
179-
align-items: center;
180-
justify-content: flex-end;
181-
gap: 0.25em;
182-
margin-bottom: 0;
183-
}
184-
185161
.outdated-summary {
186162
display: flex;
187163
flex-direction: column;
@@ -193,6 +169,40 @@
193169
color: var(--primary-medium);
194170
}
195171
}
172+
173+
.d-modal__footer {
174+
display: grid;
175+
gap: 0;
176+
grid-template-areas: "summarized regenerate" " outdated regenerate";
177+
grid-template-columns: 1fr auto;
178+
@include breakpoint(mobile-large) {
179+
gap: 0.25em 0.5em;
180+
grid-template-areas: "summarized summarized" "regenerate outdated";
181+
}
182+
183+
p {
184+
margin: 0;
185+
}
186+
187+
.fk-d-tooltip__trigger {
188+
vertical-align: text-top;
189+
}
190+
191+
.summary-outdated {
192+
color: var(--primary-high);
193+
font-size: var(--font-down-1);
194+
line-height: var(--line-height-medium);
195+
}
196+
197+
.summarized-on {
198+
grid-area: summarized;
199+
}
200+
201+
button {
202+
grid-area: regenerate;
203+
justify-self: start;
204+
}
205+
}
196206
}
197207

198208
@keyframes appear {
Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,27 @@
1-
.topic-map {
2-
.ai-summarization-button {
3-
.fk-d-menu {
4-
position: fixed;
5-
top: calc(var(--header-offset) + 1rem) !important;
6-
left: unset !important;
7-
right: 1rem !important;
8-
width: 470px;
9-
max-width: 470px !important; //overruling JS
10-
max-height: calc(
11-
100vh - var(--header-offset) - 3rem - var(--composer-height, 0px)
12-
);
1+
html.scrollable-modal {
2+
overflow: auto; // overrides core .modal-open class scroll lock
3+
}
134

14-
.ai-summary__header,
15-
.ai-summary-box {
16-
padding: 0.75em 1rem;
17-
box-sizing: border-box;
18-
}
5+
.ai-summary-modal {
6+
.d-modal__container {
7+
position: fixed;
8+
top: var(--header-offset);
9+
margin-top: 1em;
10+
right: 1em;
11+
width: 100vw;
12+
max-width: 30em;
13+
max-height: calc(
14+
100vh - var(--header-offset) - 3rem - var(--composer-height, 0px)
15+
);
1916

20-
.ai-summary {
21-
&__header {
22-
position: sticky;
23-
top: 0;
24-
display: flex;
25-
justify-content: space-between;
26-
align-items: center;
27-
padding-block: 0.5rem;
28-
padding-right: 0.5rem;
29-
background: var(--secondary);
30-
border-bottom: 1px solid var(--primary-low);
17+
box-shadow: var(--shadow-menu-panel);
18+
}
3119

32-
h3 {
33-
margin: 0;
34-
}
35-
}
36-
}
37-
}
20+
.fullscreen-composer & {
21+
display: none;
3822
}
3923
}
24+
25+
.ai-summary-modal + .d-modal__backdrop {
26+
display: none;
27+
}

0 commit comments

Comments
 (0)