Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 77 additions & 58 deletions assets/javascripts/discourse/components/ai-search-discoveries.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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;

// Clear pending animations
if (this.typingTimer) {
cancel(this.typingTimer);
this.typingTimer = null;
}

this.discobotDiscoveries.discovery = update.ai_discover_reply;
this.loadingDiscoveries = false;
} else if (newText.length > this.discobotDiscoveries.discovery.length) {
this.discobotDiscoveries.discovery = newText;
this.isStreaming = true;
await this.onTextUpdate();
}
}
}
Expand All @@ -101,29 +110,38 @@ 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";
}
}

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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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}}
>
Expand All @@ -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}} />
</div>
{{else}}
<div class="ai-search-discoveries__discovery-preview cooked">
<CookText
@rawText={{this.discobotDiscoveries.discoveryPreview}}
/>
<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>
{{/if}}
</article>

{{#if this.toggleMakesSense}}
{{#if this.canShowExpandtoggle}}
<DButton
class="btn-flat btn-text ai-search-discoveries__toggle"
@label={{this.toggleLabel}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
Expand Down
2 changes: 0 additions & 2 deletions assets/javascripts/discourse/services/discobot-discoveries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

.ai-search-discoveries {
&__regular-results-title {
margin-bottom: 0;
Expand All @@ -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;
}
}
}

Expand Down
Loading