mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-03-01 06:49:30 +00:00
UX: Smoother streaming for discoveries (#1154)
## 🔍 Overview This update ensures that the streaming for discoveries is smoother, especially on first update. ## ➕ More details To help with smoother streaming, the discovery preview (which was being tracked as a separate property in the JS logic) will be removed and the entire discovery content will be shown/hidden via the existing CSS. The preview was already receiving the full update even though it was visually hidden, so removing the separate property shouldn't have any negative performance hit. Visually hiding it with CSS only will help simplify the component and also allow for smoother streaming. We will instead remove the buffered streaming approach and instead use typing timers similar to what we did for streaming summarization. No related tests as streaming animations are difficult to test.
This commit is contained in:
parent
aa13d16022
commit
e15952031d
@ -2,39 +2,20 @@ import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { cancel, later } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import CookText from "discourse/components/cook-text";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AiBlinkingAnimation from "./ai-blinking-animation";
|
||||
|
||||
const DISCOVERY_TIMEOUT_MS = 10000;
|
||||
const BUFFER_WORDS_COUNT = 50;
|
||||
|
||||
function setUpBuffer(discovery, bufferTarget) {
|
||||
const paragraphs = discovery.split(/\n+/);
|
||||
let wordCount = 0;
|
||||
const paragraphBuffer = [];
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
const wordsInParagraph = paragraph.split(/\s+/);
|
||||
wordCount += wordsInParagraph.length;
|
||||
|
||||
if (wordCount >= bufferTarget) {
|
||||
paragraphBuffer.push(paragraph.concat("..."));
|
||||
return paragraphBuffer.join("\n");
|
||||
} else {
|
||||
paragraphBuffer.push(paragraph);
|
||||
paragraphBuffer.push("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
const STREAMED_TEXT_SPEED = 23;
|
||||
|
||||
export default class AiSearchDiscoveries extends Component {
|
||||
@service search;
|
||||
@ -43,9 +24,36 @@ export default class AiSearchDiscoveries extends Component {
|
||||
|
||||
@tracked loadingDiscoveries = false;
|
||||
@tracked hideDiscoveries = false;
|
||||
@tracked fulldiscoveryToggled = false;
|
||||
@tracked fullDiscoveryToggled = false;
|
||||
@tracked discoveryPreviewLength = this.args.discoveryPreviewLength || 150;
|
||||
|
||||
@tracked isStreaming = false;
|
||||
@tracked streamedText = "";
|
||||
|
||||
discoveryTimeout = null;
|
||||
typingTimer = null;
|
||||
streamedTextLength = 0;
|
||||
|
||||
typeCharacter() {
|
||||
if (this.streamedTextLength < this.discobotDiscoveries.discovery.length) {
|
||||
this.streamedText += this.discobotDiscoveries.discovery.charAt(
|
||||
this.streamedTextLength
|
||||
);
|
||||
this.streamedTextLength++;
|
||||
|
||||
this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED);
|
||||
} else {
|
||||
this.typingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onTextUpdate() {
|
||||
if (this.typingTimer) {
|
||||
cancel(this.typingTimer);
|
||||
}
|
||||
|
||||
this.typeCharacter();
|
||||
}
|
||||
|
||||
@bind
|
||||
async _updateDiscovery(update) {
|
||||
@ -54,28 +62,29 @@ export default class AiSearchDiscoveries extends Component {
|
||||
cancel(this.discoveryTimeout);
|
||||
}
|
||||
|
||||
if (!this.discobotDiscoveries.discoveryPreview) {
|
||||
const buffered = setUpBuffer(
|
||||
update.ai_discover_reply,
|
||||
BUFFER_WORDS_COUNT
|
||||
);
|
||||
if (buffered) {
|
||||
this.discobotDiscoveries.discoveryPreview = buffered;
|
||||
this.loadingDiscoveries = false;
|
||||
}
|
||||
if (!this.discobotDiscoveries.discovery) {
|
||||
this.discobotDiscoveries.discovery = "";
|
||||
}
|
||||
|
||||
const newText = update.ai_discover_reply;
|
||||
this.discobotDiscoveries.modelUsed = update.model_used;
|
||||
this.discobotDiscoveries.discovery = update.ai_discover_reply;
|
||||
this.loadingDiscoveries = false;
|
||||
|
||||
// Handling short replies.
|
||||
if (update.done) {
|
||||
if (!this.discobotDiscoveries.discoveryPreview) {
|
||||
this.discobotDiscoveries.discoveryPreview = update.ai_discover_reply;
|
||||
}
|
||||
this.discobotDiscoveries.discovery = newText;
|
||||
this.streamedText = newText;
|
||||
this.isStreaming = false;
|
||||
|
||||
this.discobotDiscoveries.discovery = update.ai_discover_reply;
|
||||
this.loadingDiscoveries = false;
|
||||
// Clear pending animations
|
||||
if (this.typingTimer) {
|
||||
cancel(this.typingTimer);
|
||||
this.typingTimer = null;
|
||||
}
|
||||
} else if (newText.length > this.discobotDiscoveries.discovery.length) {
|
||||
this.discobotDiscoveries.discovery = newText;
|
||||
this.isStreaming = true;
|
||||
await this.onTextUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,7 +110,7 @@ export default class AiSearchDiscoveries extends Component {
|
||||
}
|
||||
|
||||
get toggleLabel() {
|
||||
if (this.fulldiscoveryToggled) {
|
||||
if (this.fullDiscoveryToggled) {
|
||||
return "discourse_ai.discobot_discoveries.collapse";
|
||||
} else {
|
||||
return "discourse_ai.discobot_discoveries.tell_me_more";
|
||||
@ -109,21 +118,30 @@ export default class AiSearchDiscoveries extends Component {
|
||||
}
|
||||
|
||||
get toggleIcon() {
|
||||
if (this.fulldiscoveryToggled) {
|
||||
if (this.fullDiscoveryToggled) {
|
||||
return "chevron-up";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get toggleMakesSense() {
|
||||
get canShowExpandtoggle() {
|
||||
return (
|
||||
this.discobotDiscoveries.discoveryPreview &&
|
||||
this.discobotDiscoveries.discoveryPreview !==
|
||||
this.discobotDiscoveries.discovery
|
||||
!this.loadingDiscoveries &&
|
||||
this.renderedDiscovery.length > this.discoveryPreviewLength
|
||||
);
|
||||
}
|
||||
|
||||
get renderedDiscovery() {
|
||||
return this.isStreaming
|
||||
? this.streamedText
|
||||
: this.discobotDiscoveries.discovery;
|
||||
}
|
||||
|
||||
get renderPreviewOnly() {
|
||||
return !this.fullDiscoveryToggled && this.canShowExpandtoggle;
|
||||
}
|
||||
|
||||
@action
|
||||
async triggerDiscovery() {
|
||||
if (this.discobotDiscoveries.lastQuery === this.query) {
|
||||
@ -153,12 +171,11 @@ export default class AiSearchDiscoveries extends Component {
|
||||
|
||||
@action
|
||||
toggleDiscovery() {
|
||||
this.fulldiscoveryToggled = !this.fulldiscoveryToggled;
|
||||
this.fullDiscoveryToggled = !this.fullDiscoveryToggled;
|
||||
}
|
||||
|
||||
timeoutDiscovery() {
|
||||
this.loadingDiscoveries = false;
|
||||
this.discobotDiscoveries.discoveryPreview = "";
|
||||
this.discobotDiscoveries.discovery = "";
|
||||
|
||||
this.discobotDiscoveries.discoveryTimedOut = true;
|
||||
@ -167,7 +184,8 @@ export default class AiSearchDiscoveries extends Component {
|
||||
<template>
|
||||
<div
|
||||
class="ai-search-discoveries"
|
||||
{{didInsert this.subscribe}}
|
||||
{{didInsert this.subscribe @searchTerm}}
|
||||
{{didUpdate this.subscribe @searchTerm}}
|
||||
{{didInsert this.triggerDiscovery this.query}}
|
||||
{{willDestroy this.unsubscribe}}
|
||||
>
|
||||
@ -177,19 +195,20 @@ export default class AiSearchDiscoveries extends Component {
|
||||
{{else if this.discobotDiscoveries.discoveryTimedOut}}
|
||||
{{i18n "discourse_ai.discobot_discoveries.timed_out"}}
|
||||
{{else}}
|
||||
{{#if this.fulldiscoveryToggled}}
|
||||
<div class="ai-search-discoveries__discovery-full cooked">
|
||||
<CookText @rawText={{this.discobotDiscoveries.discovery}} />
|
||||
<article
|
||||
class={{concatClass
|
||||
"ai-search-discoveries__discovery"
|
||||
(if this.renderPreviewOnly "preview")
|
||||
(if this.isStreaming "streaming")
|
||||
"streamable-content"
|
||||
}}
|
||||
>
|
||||
<div class="cooked">
|
||||
<CookText @rawText={{this.renderedDiscovery}} />
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="ai-search-discoveries__discovery-preview cooked">
|
||||
<CookText
|
||||
@rawText={{this.discobotDiscoveries.discoveryPreview}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</article>
|
||||
|
||||
{{#if this.toggleMakesSense}}
|
||||
{{#if this.canShowExpandtoggle}}
|
||||
<DButton
|
||||
class="btn-flat btn-text ai-search-discoveries__toggle"
|
||||
@label={{this.toggleLabel}}
|
||||
|
@ -49,7 +49,7 @@ export default class AiDiscobotDiscoveries extends Component {
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<AiSearchDiscoveries />
|
||||
<AiSearchDiscoveries @discoveryPreviewLength={{50}} />
|
||||
|
||||
<h3 class="ai-search-discoveries__regular-results-title">
|
||||
{{icon "bars-staggered"}}
|
||||
|
@ -4,14 +4,12 @@ import Service from "@ember/service";
|
||||
export default class DiscobotDiscoveries extends Service {
|
||||
// We use this to retain state after search menu gets closed.
|
||||
// Similar to discourse/discourse#25504
|
||||
@tracked discoveryPreview = "";
|
||||
@tracked discovery = "";
|
||||
@tracked lastQuery = "";
|
||||
@tracked discoveryTimedOut = false;
|
||||
@tracked modelUsed = "";
|
||||
|
||||
resetDiscovery() {
|
||||
this.discoveryPreview = "";
|
||||
this.discovery = "";
|
||||
this.discoveryTimedOut = false;
|
||||
this.modelUsed = "";
|
||||
|
@ -1,3 +1,12 @@
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-search-discoveries {
|
||||
&__regular-results-title {
|
||||
margin-bottom: 0;
|
||||
@ -7,19 +16,23 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__discovery-preview {
|
||||
height: 3.5em; // roughly the loading skeleton height
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
&__discovery {
|
||||
&.preview {
|
||||
height: 3.5em; // roughly the loading skeleton height
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0), var(--secondary));
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0), var(--secondary));
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
animation: fade-in 0.5s ease-in forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user