Compare commits

..
Author SHA1 Message Date
Keegan George 943aa7468d FIX: Final summary sometimes missing
This PR fixes an issue where the summary updater sometimes doesn't show the last update. Due to some timing issues with the summary updater and the component this seems to happen. By explicitly calling set raw after the cooked, we ensure the final summary will always be set.

No tests as this timing issue is difficult to test.
2024-11-06 12:00:35 -08:00
139 changed files with 1828 additions and 4453 deletions
@@ -6,14 +6,6 @@ module DiscourseAi
requires_plugin ::DiscourseAi::PLUGIN_NAME
requires_login
def show_debug_info_by_id
log = AiApiAuditLog.find(params[:id])
raise Discourse::NotFound if !log.topic
guardian.ensure_can_debug_ai_bot_conversation!(log.topic)
render json: AiApiAuditLogSerializer.new(log, root: false), status: 200
end
def show_debug_info
post = Post.find(params[:post_id])
guardian.ensure_can_debug_ai_bot_conversation!(post)
+13 -27
View File
@@ -10,48 +10,34 @@ module ::Jobs
return if !SiteSetting.ai_summarization_enabled
return if SiteSetting.ai_summary_backfill_maximum_topics_per_hour.zero?
system_user = Discourse.system_user
# Split budget in 12 intervals, but make sure is at least one.
limit_per_job = [SiteSetting.ai_summary_backfill_maximum_topics_per_hour, 12].max / 12
budget = [current_budget, limit_per_job].min
complete_t = AiSummary.summary_types[:complete]
backfill_candidates(complete_t)
.limit(current_budget(complete_t))
backfill_candidates
.limit(budget)
.each do |topic|
DiscourseAi::Summarization.topic_summary(topic).force_summarize(system_user)
DiscourseAi::Summarization.topic_summary(topic).force_summarize(Discourse.system_user)
end
gist_t = AiSummary.summary_types[:gist]
backfill_candidates(gist_t)
.limit(current_budget(gist_t))
.each { |topic| DiscourseAi::Summarization.topic_gist(topic).force_summarize(system_user) }
end
def backfill_candidates(summary_type)
def backfill_candidates
Topic
.where("topics.word_count >= ?", SiteSetting.ai_summary_backfill_minimum_word_count)
.joins(<<~SQL)
LEFT OUTER JOIN ai_summaries ais ON
topics.id = ais.target_id AND
ais.target_type = 'Topic' AND
ais.summary_type = '#{summary_type}'
SQL
.joins(
"LEFT OUTER JOIN ai_summaries ais ON topics.id = ais.target_id AND ais.target_type = 'Topic'",
)
.where(
"ais.id IS NULL OR UPPER(ais.content_range) < topics.highest_post_number + 1",
) # (1..1) gets stored ad (1..2).
.order("ais.created_at DESC NULLS FIRST, topics.last_posted_at DESC")
end
def current_budget(type)
# Split budget in 12 intervals, but make sure is at least one.
def current_budget
base_budget = SiteSetting.ai_summary_backfill_maximum_topics_per_hour
limit_per_job = [base_budget, 12].max / 12
used_budget = AiSummary.complete.system.where("created_at > ?", 1.hour.ago).count
used_budget =
AiSummary.system.where("created_at > ?", 1.hour.ago).where(summary_type: type).count
current_budget = [(base_budget - used_budget), limit_per_job].min
return 0 if current_budget < 0
current_budget
base_budget - used_budget
end
end
end
-8
View File
@@ -14,14 +14,6 @@ class AiApiAuditLog < ActiveRecord::Base
Ollama = 7
SambaNova = 8
end
def next_log_id
self.class.where("id > ?", id).where(topic_id: topic_id).order(id: :asc).pluck(:id).first
end
def prev_log_id
self.class.where("id < ?", id).where(topic_id: topic_id).order(id: :desc).pluck(:id).first
end
end
# == Schema Information
+1 -10
View File
@@ -95,16 +95,7 @@ class LlmModel < ActiveRecord::Base
end
def seeded?
id.present? && id < 0
end
def api_key
if seeded?
env_key = "DISCOURSE_AI_SEEDED_LLM_API_KEY_#{id.abs}"
ENV[env_key] || self[:api_key]
else
self[:api_key]
end
id < 0
end
private
@@ -12,7 +12,5 @@ class AiApiAuditLogSerializer < ApplicationSerializer
:post_id,
:feature_name,
:language_model,
:created_at,
:prev_log_id,
:next_log_id
:created_at
end
@@ -1,6 +1,5 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import bodyClass from "discourse/helpers/body-class";
export default class AiTopicGist extends Component {
@service router;
@@ -22,7 +21,6 @@ export default class AiTopicGist extends Component {
<template>
{{#if this.showGist}}
{{bodyClass "--topic-list-with-gist"}}
<div class="ai-topic-gist">
<div class="ai-topic-gist__text">
{{@topic.ai_topic_gist}}
@@ -7,7 +7,6 @@ import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { clipboardCopy, escapeExpression } from "discourse/lib/utilities";
import i18n from "discourse-common/helpers/i18n";
import discourseLater from "discourse-common/lib/later";
@@ -64,28 +63,6 @@ export default class DebugAiModal extends Component {
this.copy(this.info.raw_response_payload);
}
async loadLog(logId) {
try {
await ajax(`/discourse-ai/ai-bot/show-debug-info/${logId}.json`).then(
(result) => {
this.info = result;
}
);
} catch (e) {
popupAjaxError(e);
}
}
@action
prevLog() {
this.loadLog(this.info.prev_log_id);
}
@action
nextLog() {
this.loadLog(this.info.next_log_id);
}
copy(text) {
clipboardCopy(text);
this.justCopiedText = I18n.t("discourse_ai.ai_bot.conversation_shared");
@@ -96,13 +73,11 @@ export default class DebugAiModal extends Component {
}
loadApiRequestInfo() {
ajax(`/discourse-ai/ai-bot/post/${this.args.model.id}/show-debug-info.json`)
.then((result) => {
this.info = result;
})
.catch((e) => {
popupAjaxError(e);
});
ajax(
`/discourse-ai/ai-bot/post/${this.args.model.id}/show-debug-info.json`
).then((result) => {
this.info = result;
});
}
get requestActive() {
@@ -172,22 +147,6 @@ export default class DebugAiModal extends Component {
@action={{this.copyResponse}}
@label="discourse_ai.ai_bot.debug_ai_modal.copy_response"
/>
{{#if this.info.prev_log_id}}
<DButton
class="btn"
@icon="angles-left"
@action={{this.prevLog}}
@label="discourse_ai.ai_bot.debug_ai_modal.previous_log"
/>
{{/if}}
{{#if this.info.next_log_id}}
<DButton
class="btn"
@icon="angles-right"
@action={{this.nextLog}}
@label="discourse_ai.ai_bot.debug_ai_modal.next_log"
/>
{{/if}}
<span class="ai-debut-modal__just-copied">{{this.justCopiedText}}</span>
</:footer>
</DModal>
@@ -1,37 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default class AiCancelStreamingButton extends Component {
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
static async cancelStreaming(post) {
try {
await ajax(`/discourse-ai/ai-bot/post/${post.id}/stop-streaming`, {
type: "POST",
});
document
.querySelector(`#post_${post.post_number}`)
.classList.remove("streaming");
} catch (e) {
popupAjaxError(e);
}
}
@action
cancelStreaming() {
this.constructor.cancelStreaming(this.args.post);
}
<template>
<DButton
class="post-action-menu__ai-cancel-streaming cancel-streaming"
...attributes
@action={{this.cancelStreaming}}
@icon="pause"
@title="discourse_ai.ai_bot.cancel_streaming"
/>
</template>
}
@@ -1,34 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { isPostFromAiBot } from "../../lib/ai-bot-helper";
import DebugAiModal from "../modal/debug-ai-modal";
export default class AiDebugButton extends Component {
static shouldRender(args) {
return isPostFromAiBot(args.post, args.state.currentUser);
}
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
static debugAiResponse(post, modal) {
modal.show(DebugAiModal, { model: post });
}
@service modal;
@action
debugAiResponse() {
this.constructor.debugAiResponse(this.args.post, this.modal);
}
<template>
<DButton
class="post-action-menu__debug-ai"
...attributes
@action={{this.debugAiResponse}}
@icon="info"
@title="discourse_ai.ai_bot.debug_ai"
/>
</template>
}
@@ -1,46 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { isPostFromAiBot } from "../../lib/ai-bot-helper";
import copyConversation from "../../lib/copy-conversation";
import ShareModal from "../modal/share-modal";
const AUTO_COPY_THRESHOLD = 4;
export default class AiDebugButton extends Component {
static shouldRender(args) {
return isPostFromAiBot(args.post, args.state.currentUser);
}
// TODO (glimmer-post-menu): Remove this static function and move the code into the button action after the widget code is removed
static async shareAiResponse(post, modal, showFeedback) {
if (post.post_number <= AUTO_COPY_THRESHOLD) {
await copyConversation(post.topic, 1, post.post_number);
showFeedback("discourse_ai.ai_bot.conversation_shared");
} else {
modal.show(ShareModal, { model: post });
}
}
@service modal;
@action
shareAiResponse() {
this.constructor.shareAiResponse(
this.args.post,
this.modal,
this.args.showFeedback
);
}
<template>
<DButton
class="post-action-menu__share-ai"
...attributes
@action={{this.shareAiResponse}}
@icon="far-copy"
@title="discourse_ai.ai_bot.share"
/>
</template>
}
@@ -24,7 +24,6 @@ export default class RagUploader extends Component {
uppyUpload = new UppyUpload(getOwner(this), {
id: "discourse-ai-rag-uploader",
maxFiles: 20,
type: "discourse_ai_rag_upload",
uploadUrl:
"/admin/plugins/discourse-ai/rag-document-fragments/files/upload",
preventDirectS3Uploads: true,
@@ -18,20 +18,19 @@ export default class SemanticSearch extends Component {
return siteSettings.ai_embeddings_semantic_search_enabled;
}
@service router;
@service appEvents;
@service siteSettings;
@service searchPreferencesManager;
@tracked searching = false;
@tracked AiResults = [];
@tracked showingAiResults = false;
@tracked AIResults = [];
@tracked showingAIResults = false;
initialSearchTerm = this.args.outletArgs.search;
get disableToggleSwitch() {
if (
this.searching ||
this.AiResults.length === 0 ||
this.AIResults.length === 0 ||
this.args.outletArgs.sortOrder !== 0
) {
return true;
@@ -40,19 +39,19 @@ export default class SemanticSearch extends Component {
get searchStateText() {
// Search results:
if (this.AiResults.length > 0) {
if (this.showingAiResults) {
if (this.AIResults.length > 0) {
if (this.showingAIResults) {
return I18n.t(
"discourse_ai.embeddings.semantic_search_results.toggle",
{
count: this.AiResults.length,
count: this.AIResults.length,
}
);
} else {
return I18n.t(
"discourse_ai.embeddings.semantic_search_results.toggle_hidden",
{
count: this.AiResults.length,
count: this.AIResults.length,
}
);
}
@@ -65,14 +64,14 @@ export default class SemanticSearch extends Component {
// Typing to search:
if (
this.AiResults.length === 0 &&
this.AIResults.length === 0 &&
this.searchTerm !== this.initialSearchTerm
) {
return I18n.t("discourse_ai.embeddings.semantic_search_results.new");
}
// No results:
if (this.AiResults.length === 0) {
if (this.AIResults.length === 0) {
return I18n.t("discourse_ai.embeddings.semantic_search_results.none");
}
}
@@ -94,19 +93,19 @@ export default class SemanticSearch extends Component {
}
@action
toggleAiResults() {
if (this.showingAiResults) {
toggleAIResults() {
if (this.showingAIResults) {
this.args.outletArgs.addSearchResults([], "topic_id");
} else {
this.args.outletArgs.addSearchResults(this.AiResults, "topic_id");
this.args.outletArgs.addSearchResults(this.AIResults, "topic_id");
}
this.showingAiResults = !this.showingAiResults;
this.showingAIResults = !this.showingAIResults;
}
@action
resetAiResults() {
this.AiResults = [];
this.showingAiResults = false;
resetAIResults() {
this.AIResults = [];
this.showingAIResults = false;
this.args.outletArgs.addSearchResults([], "topic_id");
}
@@ -120,12 +119,19 @@ export default class SemanticSearch extends Component {
return this.performHyDESearch();
}
this.#resetAndSearchOnEvent();
withPluginApi("1.15.0", (api) => {
api.onAppEvent("full-page-search:trigger-search", () => {
if (!this.searching) {
this.resetAIResults();
return this.performHyDESearch();
}
});
});
}
performHyDESearch() {
this.searching = true;
this.resetAiResults();
this.resetAIResults();
ajax("/discourse-ai/embeddings/semantic-search", {
data: { q: this.searchTerm },
@@ -139,37 +145,15 @@ export default class SemanticSearch extends Component {
}
model.posts.forEach((post) => {
post.generatedByAi = true;
post.generatedByAI = true;
});
this.AiResults = model.posts;
this.AIResults = model.posts;
})
.finally(() => (this.searching = false));
}
#resetAndSearchOnEvent() {
return withPluginApi("1.15.0", (api) => {
api.onAppEvent("full-page-search:trigger-search", () => {
if (!this.searching) {
this.resetAiResults();
return this.performHyDESearch();
}
});
});
}
@action
checkQueryParamsAndSearch() {
// This check is necessary because handleSearch() isn't called
// if query params are present and a new search has appended text.
// It ensures AiResults are reset and searched for properly
const searchQueryParam = this.router.currentRoute?.queryParams?.q;
if (searchQueryParam) {
this.#resetAndSearchOnEvent();
}
}
<template>
<span {{didInsert this.checkQueryParamsAndSearch}}></span>
{{#if this.searchEnabled}}
<div class="semantic-search__container search-results" role="region">
<div class="semantic-search__results" {{didInsert this.handleSearch}}>
@@ -179,9 +163,10 @@ export default class SemanticSearch extends Component {
>
<DToggleSwitch
disabled={{this.disableToggleSwitch}}
@state={{this.showingAiResults}}
@state={{this.showingAIResults}}
title="AI search results hidden"
class="semantic-search__results-toggle"
{{on "click" this.toggleAiResults}}
{{on "click" this.toggleAIResults}}
/>
<div class="semantic-search__searching-text">
@@ -5,9 +5,8 @@ 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 { next } 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";
@@ -19,8 +18,8 @@ import I18n from "discourse-i18n";
import DMenu from "float-kit/components/d-menu";
import DTooltip from "float-kit/components/d-tooltip";
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
const STREAMED_TEXT_SPEED = 15;
import streamUpdaterText from "../../lib/ai-streamer/progress-handlers";
import SummaryUpdater from "../../lib/ai-streamer/updaters/summary-updater";
export default class AiSummaryBox extends Component {
@service siteSettings;
@@ -36,10 +35,8 @@ export default class AiSummaryBox extends Component {
@tracked canRegenerate = false;
@tracked loading = false;
@tracked isStreaming = false;
@tracked streamedText = "";
@tracked currentIndex = 0;
typingTimer = null;
streamedTextLength = 0;
oldRaw = null; // used for comparison in SummaryUpdater in lib/ai-streamer/updaters
finalSummary = null;
get outdatedSummaryWarningText() {
let outdatedText = I18n.t("summary.outdated");
@@ -55,8 +52,6 @@ export default class AiSummaryBox extends Component {
}
resetSummary() {
this.streamedText = "";
this.currentIndex = 0;
this.text = "";
this.summarizedOn = null;
this.summarizedBy = null;
@@ -88,7 +83,8 @@ export default class AiSummaryBox extends Component {
}
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
this._channel = channel;
this.messageBus.subscribe(channel, this._updateSummary);
// we attempt to recover the last message in the bus so we subscrcibe at -2
this.messageBus.subscribe(channel, this._updateSummary, -2);
}
@bind
@@ -123,8 +119,6 @@ export default class AiSummaryBox extends Component {
}
}
// ensure summary is reset before requesting a new one:
this.resetSummary();
return this._requestSummary(fetchURL);
}
@@ -140,63 +134,36 @@ export default class AiSummaryBox extends Component {
return ajax(url).then((data) => {
if (data?.ai_topic_summary?.summarized_text) {
data.done = true;
this._updateSummary(data);
// Our streamer won't display the summary unless the summary box is in the DOM.
// Wait for the next runloop or cached summaries won't appear.
next(() => this._updateSummary(data));
}
});
}
typeCharacter() {
if (this.streamedTextLength < this.text.length) {
this.streamedText += this.text.charAt(this.streamedTextLength);
this.streamedTextLength++;
this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED);
} else {
this.typingTimer = null;
}
}
onTextUpdate() {
// Reset only if there’s a new summary to process
if (this.typingTimer) {
cancel(this.typingTimer);
}
this.typeCharacter();
}
@bind
async _updateSummary(update) {
_updateSummary(update) {
const topicSummary = {
done: update.done,
raw: update.ai_topic_summary?.summarized_text,
...update.ai_topic_summary,
};
const newText = topicSummary.raw || "";
this.loading = false;
this.isStreaming = true;
streamUpdaterText(SummaryUpdater, topicSummary, this);
if (update.done) {
this.text = newText;
this.streamedText = newText;
this.displayedTextLength = newText.length;
this.isStreaming = false;
this.text = this.finalSummary;
this.summarizedOn = shortDateNoYear(
moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z")
);
this.summarizedBy = topicSummary.algorithm;
this.newPostsSinceSummary = topicSummary.new_posts_since_summary;
this.outdated = topicSummary.outdated;
this.newPostsSinceSummary = topicSummary.new_posts_since_summary;
this.canRegenerate = topicSummary.outdated && topicSummary.can_regenerate;
// Clear pending animations
if (this.typingTimer) {
cancel(this.typingTimer);
this.typingTimer = null;
}
} else if (newText.length > this.text.length) {
this.text = newText;
this.isStreaming = true;
this.onTextUpdate();
}
}
@@ -261,7 +228,7 @@ export default class AiSummaryBox extends Component {
<AiSummarySkeleton />
{{else}}
<div class="generated-summary cooked">
<CookText @rawText={{this.streamedText}} />
{{this.text}}
</div>
{{#if this.summarizedOn}}
<div class="summarized-on">
@@ -4,17 +4,6 @@ import Composer from "discourse/models/composer";
import I18n from "I18n";
import ShareFullTopicModal from "../components/modal/share-full-topic-modal";
const MAX_PERSONA_USER_ID = -1200;
export function isPostFromAiBot(post, currentUser) {
return (
post.user_id <= MAX_PERSONA_USER_ID ||
!!currentUser?.ai_enabled_chat_bots?.any(
(bot) => post.username === bot.username
)
);
}
export function showShareConversationModal(modal, topicId) {
ajax(`/discourse-ai/ai-bot/shared-ai-conversations/preview/${topicId}.json`)
.then((payload) => {
@@ -113,6 +113,10 @@ export async function applyProgress(status, updater) {
if (status.cooked) {
await updater.setCooked(status.cooked);
}
// Ensure the final summary is set
if (status.raw !== undefined) {
await updater.setRaw(status.raw, true);
}
updater.streaming = false;
}
@@ -0,0 +1,47 @@
import { cook } from "discourse/lib/text";
import StreamUpdater from "./stream-updater";
export default class SummaryUpdater extends StreamUpdater {
constructor(topicSummary, componentContext) {
super();
this.topicSummary = topicSummary;
this.componentContext = componentContext;
if (this.topicSummary) {
this.summaryBox = document.querySelector("article.ai-summary-box");
}
}
get element() {
return this.summaryBox;
}
set streaming(value) {
if (this.element) {
if (value) {
this.componentContext.isStreaming = true;
} else {
this.componentContext.isStreaming = false;
}
}
}
async setRaw(value, done) {
this.componentContext.oldRaw = value;
const cooked = await cook(value);
await this.setCooked(cooked);
if (done) {
this.componentContext.finalSummary = cooked;
}
}
async setCooked(value) {
this.componentContext.text = value;
}
get raw() {
return this.componentContext.oldRaw || "";
}
}
@@ -1,20 +1,18 @@
import { hbs } from "ember-cli-htmlbars";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { withPluginApi } from "discourse/lib/plugin-api";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
import AiShareButton from "../discourse/components/post-menu/ai-share-button";
import {
isPostFromAiBot,
showShareConversationModal,
} from "../discourse/lib/ai-bot-helper";
import DebugAiModal from "../discourse/components/modal/debug-ai-modal";
import ShareModal from "../discourse/components/modal/share-modal";
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
import copyConversation from "../discourse/lib/copy-conversation";
const AUTO_COPY_THRESHOLD = 4;
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
let enabledChatBotIds = [];
let allowDebug = false;
function isGPTBot(user) {
return user && enabledChatBotIds.includes(user.id);
}
@@ -24,7 +22,29 @@ function attachHeaderIcon(api) {
}
function initializeAIBotReplies(api) {
initializePauseButton(api);
api.addPostMenuButton("cancel-gpt", (post) => {
if (isGPTBot(post.user)) {
return {
icon: "pause",
action: "cancelStreaming",
title: "discourse_ai.ai_bot.cancel_streaming",
className: "btn btn-default cancel-streaming",
position: "first",
};
}
});
api.attachWidgetAction("post", "cancelStreaming", function () {
ajax(`/discourse-ai/ai-bot/post/${this.model.id}/stop-streaming`, {
type: "POST",
})
.then(() => {
document
.querySelector(`#post_${this.model.post_number}`)
.classList.remove("streaming");
})
.catch(popupAjaxError);
});
api.modifyClass("controller:topic", {
pluginId: "discourse-ai",
@@ -82,42 +102,7 @@ function initializePersonaDecorator(api) {
);
}
function initializePauseButton(api) {
const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, firstButtonKey } }) => {
if (isGPTBot(post.user)) {
dag.add("ai-cancel-gpt", AiCancelStreamingButton, {
before: firstButtonKey,
after: ["ai-share", "ai-debug"],
});
}
}
);
const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";
withSilencedDeprecations(silencedKey, () => initializePauseWidgetButton(api));
}
function initializePauseWidgetButton(api) {
api.addPostMenuButton("cancel-gpt", (post) => {
if (isGPTBot(post.user)) {
return {
icon: "pause",
action: "cancelStreaming",
title: "discourse_ai.ai_bot.cancel_streaming",
className: "btn btn-default cancel-streaming",
position: "first",
};
}
});
api.attachWidgetAction("post", "cancelStreaming", function () {
AiCancelStreamingButton.cancelStreaming(this.model);
});
}
const MAX_PERSONA_USER_ID = -1200;
function initializeDebugButton(api) {
const currentUser = api.getCurrentUser();
@@ -125,30 +110,10 @@ function initializeDebugButton(api) {
return;
}
const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, firstButtonKey } }) => {
if (post.topic?.archetype === "private_message") {
dag.add("ai-debug", AiDebugButton, {
before: firstButtonKey,
after: "ai-share",
});
}
}
);
const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";
withSilencedDeprecations(silencedKey, () => initializeDebugWidgetButton(api));
}
function initializeDebugWidgetButton(api) {
const currentUser = api.getCurrentUser();
let debugAiResponse = async function ({ post }) {
const modal = api.container.lookup("service:modal");
AiDebugButton.debugAiResponse(post, modal);
modal.show(DebugAiModal, { model: post });
};
api.addPostMenuButton("debugAi", (post) => {
@@ -156,8 +121,15 @@ function initializeDebugWidgetButton(api) {
return;
}
if (!isPostFromAiBot(post, currentUser)) {
return;
if (
!currentUser.ai_enabled_chat_bots.any(
(bot) => post.username === bot.username
)
) {
// special handling for personas (persona bot users start at ID -1200 and go down)
if (post.user_id > MAX_PERSONA_USER_ID) {
return;
}
}
return {
@@ -176,29 +148,14 @@ function initializeShareButton(api) {
return;
}
const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, firstButtonKey } }) => {
if (post.topic?.archetype === "private_message") {
dag.add("ai-share", AiShareButton, {
before: firstButtonKey,
});
}
}
);
const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";
withSilencedDeprecations(silencedKey, () => initializeShareWidgetButton(api));
}
function initializeShareWidgetButton(api) {
const currentUser = api.getCurrentUser();
let shareAiResponse = async function ({ post, showFeedback }) {
const modal = api.container.lookup("service:modal");
AiShareButton.shareAiResponse(post, modal, showFeedback);
if (post.post_number <= AUTO_COPY_THRESHOLD) {
await copyConversation(post.topic, 1, post.post_number);
showFeedback("discourse_ai.ai_bot.conversation_shared");
} else {
const modal = api.container.lookup("service:modal");
modal.show(ShareModal, { model: post });
}
};
api.addPostMenuButton("share", (post) => {
@@ -207,14 +164,21 @@ function initializeShareWidgetButton(api) {
return;
}
if (!isPostFromAiBot(post, currentUser)) {
return;
if (
!currentUser.ai_enabled_chat_bots.any(
(bot) => post.username === bot.username
)
) {
// special handling for personas (persona bot users start at ID -1200 and go down)
if (post.user_id > MAX_PERSONA_USER_ID) {
return;
}
}
return {
action: shareAiResponse,
icon: "far-copy",
className: "post-action-menu__share-ai",
className: "post-action-menu__share",
title: "discourse_ai.ai_bot.share",
position: "first",
};
@@ -254,10 +218,10 @@ export default {
enabledChatBotIds = user.ai_enabled_chat_bots.map((bot) => bot.id);
allowDebug = user.can_debug_ai_bot_conversations;
withPluginApi("1.6.0", attachHeaderIcon);
withPluginApi("1.34.0", initializeAIBotReplies);
withPluginApi("1.6.0", initializeAIBotReplies);
withPluginApi("1.6.0", initializePersonaDecorator);
withPluginApi("1.34.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.34.0", (api) => initializeShareButton(api, container));
withPluginApi("1.22.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
withPluginApi("1.22.0", (api) =>
initializeShareTopicButton(api, container)
);
@@ -4,6 +4,6 @@ export default apiInitializer("1.15.0", (api) => {
api.modifyClass("component:search-result-entry", {
pluginId: "discourse-ai",
classNameBindings: ["bulkSelectEnabled", "post.generatedByAi:ai-result"],
classNameBindings: ["bulkSelectEnabled", "post.generatedByAI:ai-result"],
});
});
@@ -1,7 +1,6 @@
import { cached, tracked } from "@glimmer/tracking";
import BasicTopicList from "discourse/components/basic-topic-list";
import { withPluginApi } from "discourse/lib/plugin-api";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
const RelatedTopics = <template>
@@ -12,7 +11,7 @@ const RelatedTopics = <template>
class="more-topics__list"
>
<h3 id="related-topics-title" class="more-topics__list-title">
{{icon "discourse-sparkles"}}{{i18n "discourse_ai.related_topics.title"}}
{{i18n "discourse_ai.related_topics.title"}}
</h3>
<div class="topics">
<BasicTopicList @topics={{@topic.relatedTopics}} />
@@ -1,15 +1,3 @@
.related-topics {
margin: 4.5em 0 1em;
}
.more-topics__container {
h3 .d-icon {
margin-right: 0.25em;
color: var(--primary-high);
font-size: var(--font-down-1);
}
.nav-pills .d-icon {
font-size: var(--font-down-1);
}
}
@@ -224,58 +224,22 @@
line-height: 1.4;
width: 90%;
}
.desktop-view & {
margin-top: 0.15em;
margin-bottom: -0.15em;
}
&__text {
color: var(--primary-high);
text-wrap: pretty;
word-wrap: break-word;
font-size: var(--font-down-1);
max-width: 65ch;
max-width: 70ch;
.visited & {
color: var(--primary-medium);
}
}
}
.--topic-list-with-gist {
.topic-list-item {
.main-link {
line-height: var(--line-height-medium);
}
.link-bottom-line {
font-size: var(--font-down-1);
margin-top: 0.25em;
}
.ai-topic-gist {
font-size: var(--font-up-1);
line-height: var(--line-height-large);
margin-top: 0.25em;
margin-bottom: 0.15em;
}
.topic-post-badges {
font-size: var(--font-down-1);
}
}
.mobile-view & {
.topic-list {
.topic-list-item > .topic-list-data {
padding: 1em 0;
}
.topic-item-stats .num.activity {
align-self: end;
margin-bottom: -0.15em; // vertical alignment
}
.pull-left {
padding-top: 0.25em;
}
.right {
margin-left: 3.75em;
}
}
}
}
.ai-gists-dropdown-trigger {
font-size: var(--font-down-1);
color: var(--primary-medium);
-2
View File
@@ -333,8 +333,6 @@ ar:
response_tokens: "رموز الرد:"
request: "طلب"
response: "الرد"
next_log: "التالي"
previous_log: "السابق"
share_full_topic_modal:
title: "مشاركة المحادثة بشكلٍ علني"
share: "مشاركة ونسخ الرابط"
-1
View File
@@ -77,6 +77,5 @@ be:
ai_bot:
debug_ai_modal:
request: "запыт"
next_log: "Далей"
share_modal:
copy: "капіяваць"
-2
View File
@@ -80,8 +80,6 @@ bg:
ai_bot:
debug_ai_modal:
request: "Заявка"
next_log: "Напред"
previous_log: "Предишни"
share_modal:
copy: "Копирай"
summarization:
-2
View File
@@ -88,7 +88,5 @@ bs_BA:
debug_ai_modal:
request: "Zatraži"
response: "Odgovor"
next_log: "Iduće"
previous_log: "Previous"
share_modal:
copy: "Copy"
-2
View File
@@ -89,7 +89,5 @@ ca:
debug_ai_modal:
request: "Sol·licita"
response: "Reacció"
next_log: "Següent"
previous_log: "Previ"
share_modal:
copy: "Còpia"
-2
View File
@@ -92,8 +92,6 @@ cs:
debug_ai_modal:
request: "Požadavek"
response: "Odpověď"
next_log: "Další"
previous_log: "Předchozí"
share_modal:
copy: "Kopírovat"
summarization:
-2
View File
@@ -91,7 +91,5 @@ da:
debug_ai_modal:
request: "Anmod om medlemskab"
response: "Svar"
next_log: "Næste"
previous_log: "Forrige"
share_modal:
copy: "Kopier"
+2 -7
View File
@@ -11,8 +11,7 @@ de:
scopes:
descriptions:
discourse_ai:
search: "Ermöglicht semantische Suche"
stream_completion: "Ermöglicht das Streamen von KI-Persona-Vervollständigungen"
search: "Ermöglicht die semantische Suche über den Endpunkt /discourse-ai/embeddings/semantic-search."
site_settings:
categories:
discourse_ai: "Discourse-KI"
@@ -404,8 +403,6 @@ de:
response_tokens: "Antwort-Token:"
request: "Anfrage"
response: "Antwort"
next_log: "Nächstes"
previous_log: "Vorheriges"
share_full_topic_modal:
title: "Unterhaltung öffentlich teilen"
share: "Link teilen und kopieren"
@@ -451,9 +448,7 @@ de:
topic:
title: "Zusammenfassung des Themas"
close: "Zusammenfassungspanel schließen"
gists_enabled: "mit Zusammenfassung"
gists_enabled_long: "mit KI-generierter Zusammenfassung"
gists_disabled: "ohne Zusammenfassung"
disclosure: "Von KI erstellte Zusammenfassungen"
review:
types:
reviewable_ai_post:
-2
View File
@@ -88,7 +88,5 @@ el:
debug_ai_modal:
request: "Αίτημα"
response: "Απάντηση"
next_log: "Επόμενο"
previous_log: "Προηγούμενο"
share_modal:
copy: "Αντιγραφή"

Some files were not shown because too many files have changed in this diff Show More