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

View File

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

View File

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