discourse-ai/assets/javascripts/discourse/components/ai-search-discoveries.gjs
Keegan George a3fb5cdc21
FIX: Subsequent results in full page search should re-trigger discovery (#1156)
This update fixes an issue where subsequent results in full page search were not trigger Discobot discoveries to search for the new result.
2025-02-27 10:18:42 -08:00

249 lines
6.4 KiB
Text

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 STREAMED_TEXT_SPEED = 23;
export default class AiSearchDiscoveries extends Component {
@service search;
@service messageBus;
@service discobotDiscoveries;
@service appEvents;
@tracked loadingDiscoveries = false;
@tracked hideDiscoveries = false;
@tracked fullDiscoveryToggled = false;
@tracked discoveryPreviewLength = this.args.discoveryPreviewLength || 150;
@tracked isStreaming = false;
@tracked streamedText = "";
discoveryTimeout = null;
typingTimer = null;
streamedTextLength = 0;
constructor() {
super(...arguments);
this.appEvents.on(
"full-page-search:trigger-search",
this,
this.triggerDiscovery
);
}
willDestroy() {
super.willDestroy(...arguments);
this.appEvents.off(
"full-page-search:trigger-search",
this,
this.triggerDiscovery
);
}
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() {
this.cancelTypingTimer();
this.typeCharacter();
}
cancelTypingTimer() {
if (this.typingTimer) {
cancel(this.typingTimer);
}
}
resetStreaming() {
this.cancelTypingTimer();
this.streamedText = "";
this.streamedTextLength = 0;
}
@bind
async _updateDiscovery(update) {
if (this.query === update.query) {
if (this.discoveryTimeout) {
cancel(this.discoveryTimeout);
}
if (!this.discobotDiscoveries.discovery) {
this.discobotDiscoveries.discovery = "";
}
const newText = update.ai_discover_reply;
this.discobotDiscoveries.modelUsed = update.model_used;
this.loadingDiscoveries = false;
// Handling short replies.
if (update.done) {
this.discobotDiscoveries.discovery = newText;
this.streamedText = newText;
this.isStreaming = false;
// Clear pending animations
this.cancelTypingTimer();
} else if (newText.length > this.discobotDiscoveries.discovery.length) {
this.discobotDiscoveries.discovery = newText;
this.isStreaming = true;
await this.onTextUpdate();
}
}
}
@bind
unsubscribe() {
this.messageBus.unsubscribe(
"/discourse-ai/ai-bot/discover",
this._updateDiscovery
);
}
@bind
subscribe() {
this.messageBus.subscribe(
"/discourse-ai/ai-bot/discover",
this._updateDiscovery
);
}
get query() {
return this.args?.searchTerm || this.search.activeGlobalSearchTerm;
}
get toggleLabel() {
if (this.fullDiscoveryToggled) {
return "discourse_ai.discobot_discoveries.collapse";
} else {
return "discourse_ai.discobot_discoveries.tell_me_more";
}
}
get toggleIcon() {
if (this.fullDiscoveryToggled) {
return "chevron-up";
} else {
return "";
}
}
get canShowExpandtoggle() {
return (
!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) {
this.hideDiscoveries = false;
return;
} else {
this.resetStreaming();
this.discobotDiscoveries.resetDiscovery();
}
this.hideDiscoveries = false;
this.loadingDiscoveries = true;
this.discoveryTimeout = later(
this,
this.timeoutDiscovery,
DISCOVERY_TIMEOUT_MS
);
try {
this.discobotDiscoveries.lastQuery = this.query;
await ajax("/discourse-ai/ai-bot/discover", {
data: { query: this.query },
});
} catch {
this.hideDiscoveries = true;
}
}
@action
toggleDiscovery() {
this.fullDiscoveryToggled = !this.fullDiscoveryToggled;
}
timeoutDiscovery() {
this.loadingDiscoveries = false;
this.discobotDiscoveries.discovery = "";
this.discobotDiscoveries.discoveryTimedOut = true;
}
<template>
<div
class="ai-search-discoveries"
{{didInsert this.subscribe @searchTerm}}
{{didUpdate this.subscribe @searchTerm}}
{{didInsert this.triggerDiscovery this.query}}
{{willDestroy this.unsubscribe}}
>
<div class="ai-search-discoveries__completion">
{{#if this.loadingDiscoveries}}
<AiBlinkingAnimation />
{{else if this.discobotDiscoveries.discoveryTimedOut}}
{{i18n "discourse_ai.discobot_discoveries.timed_out"}}
{{else}}
<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>
</article>
{{#if this.canShowExpandtoggle}}
<DButton
class="btn-flat btn-text ai-search-discoveries__toggle"
@label={{this.toggleLabel}}
@icon={{this.toggleIcon}}
@action={{this.toggleDiscovery}}
/>
{{/if}}
{{/if}}
</div>
</div>
</template>
}