UX: AI composer helper refinements (#1387)

This update includes a variety of small refinements to the AI composer helper:

- prevent height jump when going from loading text placeholder → proofreading text streaming
- update padding on AI helper options list to be more suitable with typical Discourse menu design
- for composer helper results that are not `showResultAsDiff` (i.e. translation):
   - update before/after diff design to be more subtle
   - results should be in normal font (as the text is cooked and not raw markdown)
- fix: smooth streaming animation stuck showing dot icon even after smooth streaming is done
This commit is contained in:
Keegan George 2025-05-30 10:35:53 -07:00 committed by GitHub
parent e6876aabd5
commit 38f7e9c2c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 38 deletions

View File

@ -5,6 +5,7 @@ import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
import CookText from "discourse/components/cook-text";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
@ -41,6 +42,10 @@ export default class ModalDiffModal extends Component {
}
get diffResult() {
if (this.loading) {
return this.escapedSelectedText;
}
if (this.diffStreamer.diff?.length > 0) {
return this.diffStreamer.diff;
}
@ -50,10 +55,22 @@ export default class ModalDiffModal extends Component {
return this.escapedSelectedText;
}
get smoothStreamerResult() {
if (this.loading) {
return this.escapedSelectedText;
}
return this.smoothStreamer.renderedText;
}
get isStreaming() {
// diffStreamer stops Streaming when it is finished with a chunk, looking at isDone is safe
// it starts off not done
return !this.diffStreamer.isDone || this.smoothStreamer.isStreaming;
if (this.args.model.showResultAsDiff) {
return !this.diffStreamer.isDone;
}
return this.smoothStreamer.isStreaming;
}
get primaryBtnLabel() {
@ -154,42 +171,37 @@ export default class ModalDiffModal extends Component {
{{willDestroy this.cleanup}}
class="text-preview"
>
{{#if this.loading}}
<div class="composer-ai-helper-modal__loading">
{{~@model.selectedText~}}
</div>
{{else}}
<div
class={{concatClass
"composer-ai-helper-modal__suggestion"
"streamable-content"
(if this.isStreaming "streaming")
(if @model.showResultAsDiff "inline-diff")
(if this.diffStreamer.isThinking "thinking")
}}
>
{{~#if @model.showResultAsDiff~}}
<span class="diff-inner">{{htmlSafe this.diffResult}}</span>
<div
class={{concatClass
"composer-ai-helper-modal__suggestion"
"streamable-content"
(if this.isStreaming "streaming")
(if @model.showResultAsDiff "inline-diff")
(if this.diffStreamer.isThinking "thinking")
(if this.loading "composer-ai-helper-modal__loading")
}}
>
{{~#if @model.showResultAsDiff~}}
<span class="diff-inner">{{htmlSafe this.diffResult}}</span>
{{else}}
{{#if (or this.loading this.smoothStreamer.isStreaming)}}
<CookText
@rawText={{this.smoothStreamerResult}}
class="cooked"
/>
{{else}}
{{#if this.smoothStreamer.isStreaming}}
<div class="composer-ai-helper-modal__old-value">
{{~this.escapedSelectedText~}}
</div>
<div class="composer-ai-helper-modal__new-value">
<CookText
@rawText={{this.smoothStreamer.renderedText}}
@rawText={{this.smoothStreamerResult}}
class="cooked"
/>
{{else}}
<div class="composer-ai-helper-modal__old-value">
{{@model.selectedText}}
</div>
<div class="composer-ai-helper-modal__new-value">
<CookText
@rawText={{this.smoothStreamer.renderedText}}
class="cooked"
/>
</div>
{{/if}}
</div>
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
</div>
</:body>

View File

@ -111,7 +111,7 @@ mark.highlight {
animation-name: mark-blink;
}
.composer-ai-helper-modal__loading {
.composer-ai-helper-modal__loading.inline-diff {
white-space: pre-wrap;
}

View File

@ -1,11 +1,13 @@
@use "lib/viewport";
.composer-ai-helper-modal {
.text-preview,
.inline-diff {
font-family: var(--d-font-family--monospace);
font-variant-ligatures: none;
}
.text-preview,
.inline-diff {
ins {
background-color: var(--success-low);
text-decoration: none;
@ -55,13 +57,16 @@
}
&__old-value {
background-color: var(--danger-low);
white-space: pre-wrap;
border-left: 2px solid var(--danger);
padding-left: 1rem;
color: var(--danger);
margin-bottom: 1rem;
}
&__new-value {
background-color: var(--success-low);
border-left: 2px solid var(--success);
padding-left: 1rem;
color: var(--success);
}
@ -77,7 +82,6 @@
}
.ai-composer-helper-menu {
padding: 0.25rem;
max-width: 25rem;
list-style: none;
@ -701,7 +705,7 @@
width: 100%;
border-radius: 0;
margin: 0;
padding: 0.5em 1rem;
padding: 0.7rem 1rem;
&:focus,
&:hover {