UX: Use `DMenu` for topic summarization (#724)

This commit is contained in:
Jan Cernik 2024-07-25 10:47:18 -03:00 committed by GitHub
parent 5c196bca89
commit d9dad56c6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 311 additions and 265 deletions

View File

@ -13,41 +13,23 @@ 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 and from "truth-helpers/helpers/and";
import not from "truth-helpers/helpers/not";
import or from "truth-helpers/helpers/or";
import AiSummarySkeleton from "../../components/ai-summary-skeleton"; import AiSummarySkeleton from "../../components/ai-summary-skeleton";
export default class AiSummaryBox extends Component { export default class AiSummaryBox extends Component {
@service siteSettings; @service siteSettings;
@service messageBus; @service messageBus;
@service currentUser; @service currentUser;
@tracked summary = "";
@tracked text = ""; @tracked text = "";
@tracked summarizedOn = null; @tracked summarizedOn = null;
@tracked summarizedBy = null; @tracked summarizedBy = null;
@tracked newPostsSinceSummary = null; @tracked newPostsSinceSummary = null;
@tracked outdated = false; @tracked outdated = false;
@tracked canRegenerate = false; @tracked canRegenerate = false;
@tracked regenerated = false;
@tracked showSummaryBox = false;
@tracked canCollapseSummary = false;
@tracked loading = false; @tracked loading = false;
get generateSummaryTitle() {
const title = this.canRegenerate
? "summary.buttons.regenerate"
: "summary.buttons.generate";
return I18n.t(title);
}
get generateSummaryIcon() {
return this.canRegenerate ? "sync" : "discourse-sparkles";
}
get outdatedSummaryWarningText() { get outdatedSummaryWarningText() {
let outdatedText = I18n.t("summary.outdated"); let outdatedText = I18n.t("summary.outdated");
@ -65,40 +47,12 @@ export default class AiSummaryBox extends Component {
return this.args.outletArgs.postStream.summary; return this.args.outletArgs.postStream.summary;
} }
@action get topicId() {
collapse() { return this.args.outletArgs.topic.id;
this.showSummaryBox = false;
this.canCollapseSummary = false;
} }
@action get baseSummarizationURL() {
generateSummary() { return `/discourse-ai/summarization/t/${this.topicId}`;
const topicId = this.args.outletArgs.topic.id;
this.showSummaryBox = true;
if (this.text && !this.canRegenerate) {
this.canCollapseSummary = false;
return;
}
let fetchURL = `/discourse-ai/summarization/t/${topicId}?`;
if (this.currentUser) {
fetchURL += `stream=true`;
if (this.canRegenerate) {
fetchURL += "&skip_age_check=true";
}
}
this.loading = true;
return ajax(fetchURL).then((data) => {
if (!this.currentUser) {
data.done = true;
this._updateSummary(data);
}
});
} }
@bind @bind
@ -115,6 +69,49 @@ export default class AiSummaryBox extends Component {
); );
} }
@action
generateSummary() {
let fetchURL = this.baseSummarizationURL;
if (this.currentUser) {
fetchURL += `?stream=true`;
}
return this._requestSummary(fetchURL);
}
@action
regenerateSummary() {
let fetchURL = this.baseSummarizationURL;
if (this.currentUser) {
fetchURL += `?stream=true`;
if (this.canRegenerate) {
fetchURL += "&skip_age_check=true";
}
}
return this._requestSummary(fetchURL);
}
@action
_requestSummary(url) {
if (this.loading || (this.text && !this.canRegenerate)) {
return;
}
this.loading = true;
this.summarizedOn = null;
return ajax(url).then((data) => {
if (!this.currentUser) {
data.done = true;
this._updateSummary(data);
}
});
}
@bind @bind
_updateSummary(update) { _updateSummary(update) {
const topicSummary = update.ai_topic_summary; const topicSummary = update.ai_topic_summary;
@ -138,69 +135,71 @@ export default class AiSummaryBox extends Component {
} }
<template> <template>
{{#if (or @outletArgs.topic.has_summary @outletArgs.topic.summarizable)}}
<div class="summarization-buttons">
{{#if @outletArgs.topic.summarizable}} {{#if @outletArgs.topic.summarizable}}
{{#if this.showSummaryBox}}
<DButton
@action={{this.collapse}}
@title="summary.buttons.hide"
@label="summary.buttons.hide"
@icon="chevron-up"
class="btn-primary ai-topic-summarization"
/>
{{else}}
<DButton
@action={{this.generateSummary}}
@translatedLabel={{this.generateSummaryTitle}}
@translatedTitle={{this.generateSummaryTitle}}
@icon={{this.generateSummaryIcon}}
@disabled={{this.loading}}
class="btn-primary ai-topic-summarization"
/>
{{/if}}
{{/if}}
{{yield}}
</div>
<div <div
class="summary-box__container" class="ai-summarization-button"
{{didInsert this.subscribe}} {{didInsert this.subscribe}}
{{willDestroy this.unsubscribe}} {{willDestroy this.unsubscribe}}
> >
{{#if this.showSummaryBox}} <DMenu
@onShow={{this.generateSummary}}
@arrow={{true}}
@identifier="topic-map__ai-summary"
@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"
>
<:content>
<div class="ai-summary-container">
<h3>Topic Summary</h3>
<article class="ai-summary-box"> <article class="ai-summary-box">
{{#if (and this.loading (not this.text))}} {{#if this.loading}}
<AiSummarySkeleton /> <AiSummarySkeleton />
{{else}} {{else}}
<div class="generated-summary">{{this.text}}</div> <div class="generated-summary">{{this.text}}</div>
{{#if this.summarizedOn}} {{#if this.summarizedOn}}
<div class="summarized-on"> <div class="summarized-on">
<p> <p>
{{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 "info-circle"}}
</:trigger> </:trigger>
<:content> <:content>
{{i18n "summary.model_used" model=this.summarizedBy}} {{i18n
"summary.model_used"
model=this.summarizedBy
}}
</:content> </:content>
</DTooltip> </DTooltip>
</p> </p>
<div class="outdated-summary">
{{#if this.outdated}} {{#if this.outdated}}
<p class="outdated-summary"> <p>{{this.outdatedSummaryWarningText}}</p>
{{this.outdatedSummaryWarningText}}
</p>
{{/if}} {{/if}}
{{#if this.canRegenerate}}
<DButton
@label="summary.buttons.regenerate"
@title="summary.buttons.regenerate"
@action={{this.regenerateSummary}}
@icon="sync"
/>
{{/if}}
</div>
</div> </div>
{{/if}} {{/if}}
{{/if}} {{/if}}
</article> </article>
{{/if}} </div>
</:content>
</DMenu>
</div> </div>
{{/if}} {{/if}}
</template> </template>

View File

@ -1,13 +1,50 @@
.topic-map .topic-map__additional-contents { .topic-map {
.summarization-buttons { // Hide the Top Replies label if summarization is enabled
padding-bottom: 0.5em; &:has(.topic-map__additional-contents .ai-summarization-button) {
.top-replies {
.d-icon {
margin: 0;
height: 1.2em;
}
.d-button-label {
display: none;
}
}
}
// Hide the Summarize label when there are many stats unless is mobile
&:has(.--many-stats):has(.top-replies) .topic-map__additional-contents {
@media screen and (min-width: 476px) {
button {
.d-icon {
margin: 0;
height: 1.2em;
}
.d-button-label {
display: none;
}
}
}
}
.topic-map__additional-contents {
.ai-summarization-button {
padding-block: 0.5em;
display: flex;
gap: 0.5em;
button span {
white-space: nowrap;
}
}
} }
} }
.topic-map .toggle-summary { .topic-map__ai-summary-content {
.summarization-buttons { .fk-d-menu__inner-content,
display: flex; .d-modal__body {
gap: 0.5em; .ai-summary-container {
width: 100%;
} }
.ai-summary { .ai-summary {
@ -23,7 +60,7 @@
border-radius: var(--d-border-radius); border-radius: var(--d-border-radius);
margin-right: 8px; margin-right: 8px;
margin-bottom: 8px; margin-bottom: 8px;
height: 18px; height: 1em;
opacity: 0; opacity: 0;
display: block; display: block;
&:nth-child(1) { &:nth-child(1) {
@ -109,7 +146,8 @@
opacity: 1; opacity: 1;
} }
&.show { &.show {
animation: appear 0.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) 0s forwards; animation: appear 0.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) 0s
forwards;
@media (prefers-reduced-motion) { @media (prefers-reduced-motion) {
animation-duration: 0s; animation-duration: 0s;
} }
@ -153,17 +191,26 @@
width: 100%; width: 100%;
} }
.summarized-on { .summarized-on p {
text-align: right; display: flex;
align-items: center;
.info-icon { justify-content: flex-end;
margin-left: 3px; gap: 4px;
} margin-bottom: 0;
} }
.outdated-summary { .outdated-summary {
display: flex;
flex-direction: column;
align-items: flex-end;
button {
margin-top: 0.5em;
}
p {
color: var(--primary-medium); color: var(--primary-medium);
} }
}
}
} }
@keyframes ai-summary__indicator-wave { @keyframes ai-summary__indicator-wave {