Compare commits

..
Author SHA1 Message Date
Keegan George 3ce8d943a7 DEV: First version
showing message bus error, but it kinda works? wip on a fix.
2024-11-12 20:42:19 -08:00
Sam 9551b1a4d1 FIX: do not strip empty string during stream processing (#911)
Fixes issue in Open AI provider eating newlines and spaces
2024-11-13 07:12:00 +11:00
Rafael dos Santos Silva aef9a03d4c FEATURE: Truncate AI Captions to a reasonable max size (#907) 2024-11-12 15:52:46 -03:00
Sérgio Saquetim 9583964676 DEV: Added compatibility with the Glimmer Post Menu (#887) 2024-11-12 15:46:17 -03:00
Keegan George 2fc05685bb DEV: Apply text formatting and AI feature naming conventions (#905)
This PR applies Discourse's formatting text guidelines as per [documentation on Meta](https://meta.discourse.org/t/formatting-text-in-discourse-documentation-and-uis/324637). Additionally, applies our AI feature naming conventions, standardizing feature names.
2024-11-12 08:48:30 -08:00
Kris 26bfda4763 UX: reduce topic list title size when gists are enabled (#910) 2024-11-12 09:55:07 -05:00
Discourse Translator Bot 6ef635619f Update translations (#909) 2024-11-12 14:54:47 +01:00
Sam e817b7dc11 FEATURE: improve tool support (#904)
This re-implements tool support in DiscourseAi::Completions::Llm #generate

Previously tool support was always returned via XML and it would be the responsibility of the caller to parse XML

New implementation has the endpoints return ToolCall objects.

Additionally this simplifies the Llm endpoint interface and gives it more clarity. Llms must implement

decode, decode_chunk (for streaming)

It is the implementers responsibility to figure out how to decode chunks, base no longer implements. To make this easy we ship a flexible json decoder which is easy to wire up.

Also (new)

    Better debugging for PMs, we now have a next / previous button to see all the Llm messages associated with a PM
    Token accounting is fixed for vllm (we were not correctly counting tokens)
2024-11-12 08:14:30 +11:00
Keegan George 644141ff08 FIX: Regenerate summary button still shows cached summary (#903)
This PR fixes an issue where clicking to regenerate a summary was still showing the cached summary. To resolve this we call resetSummary() to reset all the summarization related properties before creating a new request.
2024-11-07 16:01:18 -08:00
Roman Rizzi fbc74c7467 FEATURE: Extend summary backfill to also generate gists (#896)
Updates default batch size to 0 and max to 10000
2024-11-07 13:40:18 -03:00
Keegan George c421f713a3 DEV: Handle streaming animation within AiSummaryBox (#901)
This PR further decouples the streaming animation by completely handling the streaming animation directly in the `AiSummaryBox` component. Previously, handling the streaming animation by calling methods in the `ai-streamer` API was leading to timing issues making things out-of-sync. This results in some issues such as the last update of streamed text not being shown. Handling streaming directly in the component should simplify things drastically and prevent any issues.
2024-11-07 08:08:32 -08:00
Rafael dos Santos Silva 021e09607d FIX: Unhide gemini api key setting for embeddings (#902) 2024-11-07 11:55:18 -03:00
Kris 893fa624e4 UX: increase gist size, and adjust surrounding elements to accommodate (#900) 2024-11-07 08:35:05 -05:00
Kris 1ad5321c09 UX: add sparkle icon to related topics for anons (#897) 2024-11-05 17:15:20 -05:00
Osama Sayegh da7c97d294 FIX: Specify type for Rag upload (#895)
Specifying `type` when using `UppyUpload` will be required as of https://github.com/discourse/discourse/pull/29600.
2024-11-05 22:10:54 +03:00
Discourse Translator Bot ab4678275d Update translations (#894) 2024-11-05 16:55:54 +01:00
Keegan George 99282612a9 DEV: Prefer ENV key for seeded models (#893)
This PR ensures we prefer getting the API key from environment variables when it is a seeded model.
2024-11-05 06:19:13 -08:00
131 changed files with 3306 additions and 1760 deletions
@@ -6,6 +6,14 @@ 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)
+27 -13
View File
@@ -10,34 +10,48 @@ module ::Jobs
return if !SiteSetting.ai_summarization_enabled
return if SiteSetting.ai_summary_backfill_maximum_topics_per_hour.zero?
# 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
system_user = Discourse.system_user
backfill_candidates
.limit(budget)
complete_t = AiSummary.summary_types[:complete]
backfill_candidates(complete_t)
.limit(current_budget(complete_t))
.each do |topic|
DiscourseAi::Summarization.topic_summary(topic).force_summarize(Discourse.system_user)
DiscourseAi::Summarization.topic_summary(topic).force_summarize(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
def backfill_candidates(summary_type)
Topic
.where("topics.word_count >= ?", SiteSetting.ai_summary_backfill_minimum_word_count)
.joins(
"LEFT OUTER JOIN ai_summaries ais ON topics.id = ais.target_id AND ais.target_type = 'Topic'",
)
.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
.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
def current_budget(type)
# Split budget in 12 intervals, but make sure is at least one.
base_budget = SiteSetting.ai_summary_backfill_maximum_topics_per_hour
used_budget = AiSummary.complete.system.where("created_at > ?", 1.hour.ago).count
limit_per_job = [base_budget, 12].max / 12
base_budget - used_budget
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
end
end
end
+8
View File
@@ -14,6 +14,14 @@ 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
+10 -1
View File
@@ -95,7 +95,16 @@ class LlmModel < ActiveRecord::Base
end
def seeded?
id < 0
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
end
private
@@ -12,5 +12,7 @@ class AiApiAuditLogSerializer < ApplicationSerializer
:post_id,
:feature_name,
:language_model,
:created_at
:created_at,
:prev_log_id,
:next_log_id
end
@@ -1,5 +1,6 @@
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;
@@ -21,6 +22,7 @@ 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,6 +7,7 @@ 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";
@@ -63,6 +64,28 @@ 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");
@@ -73,11 +96,13 @@ 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;
});
ajax(`/discourse-ai/ai-bot/post/${this.args.model.id}/show-debug-info.json`)
.then((result) => {
this.info = result;
})
.catch((e) => {
popupAjaxError(e);
});
}
get requestActive() {
@@ -147,6 +172,22 @@ 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>
@@ -0,0 +1,37 @@
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>
}
@@ -0,0 +1,34 @@
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>
}
@@ -0,0 +1,46 @@
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,6 +24,7 @@ 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,
@@ -5,8 +5,9 @@ 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 { next } from "@ember/runloop";
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";
@@ -18,8 +19,7 @@ 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";
import streamUpdaterText from "../../lib/ai-streamer/progress-handlers";
import SummaryUpdater from "../../lib/ai-streamer/updaters/summary-updater";
import smoothStreamText from "../../modifiers/smooth-stream-text";
export default class AiSummaryBox extends Component {
@service siteSettings;
@@ -35,8 +35,7 @@ export default class AiSummaryBox extends Component {
@tracked canRegenerate = false;
@tracked loading = false;
@tracked isStreaming = false;
oldRaw = null; // used for comparison in SummaryUpdater in lib/ai-streamer/updaters
finalSummary = null;
@tracked haltAnimation = false;
get outdatedSummaryWarningText() {
let outdatedText = I18n.t("summary.outdated");
@@ -83,8 +82,7 @@ export default class AiSummaryBox extends Component {
}
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
this._channel = channel;
// we attempt to recover the last message in the bus so we subscrcibe at -2
this.messageBus.subscribe(channel, this._updateSummary, -2);
this.messageBus.subscribe(channel, this._updateSummary);
}
@bind
@@ -119,6 +117,8 @@ export default class AiSummaryBox extends Component {
}
}
// ensure summary is reset before requesting a new one:
this.resetSummary();
return this._requestSummary(fetchURL);
}
@@ -134,35 +134,32 @@ export default class AiSummaryBox extends Component {
return ajax(url).then((data) => {
if (data?.ai_topic_summary?.summarized_text) {
data.done = true;
// 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));
this._updateSummary(data);
}
});
}
@bind
_updateSummary(update) {
async _updateSummary(update) {
const topicSummary = {
done: update.done,
raw: update.ai_topic_summary?.summarized_text,
...update.ai_topic_summary,
};
const newText = topicSummary.raw || "";
this.text = newText;
this.loading = false;
this.isStreaming = true;
streamUpdaterText(SummaryUpdater, topicSummary, this);
if (update.done) {
this.text = newText;
this.isStreaming = false;
this.text = this.finalSummary;
this.haltAnimation = true;
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;
}
}
@@ -227,8 +224,13 @@ export default class AiSummaryBox extends Component {
{{#if this.loading}}
<AiSummarySkeleton />
{{else}}
<div class="generated-summary cooked">
{{this.text}}
<div
class="generated-summary"
{{smoothStreamText this.text this.haltAnimation}}
>
{{#if this.haltAnimation}}
<CookText @rawText={{this.text}} />
{{/if}}
</div>
{{#if this.summarizedOn}}
<div class="summarized-on">
@@ -4,6 +4,17 @@ 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,10 +113,6 @@ 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;
}
@@ -1,47 +0,0 @@
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 || "";
}
}
@@ -0,0 +1,60 @@
import { cancel, later } from "@ember/runloop";
import { htmlSafe } from "@ember/template";
import Modifier from "ember-modifier";
import { cook } from "discourse/lib/text";
const STREAMED_TEXT_SPEED = 15;
export default class SmoothStreamTextModifier extends Modifier {
typingTimer = null;
displayedText = "";
modify(element, [text, haltAnimation]) {
if (haltAnimation) {
return;
}
this._startTypingAnimation(element, text);
}
async _startTypingAnimation(element, text) {
if (this.typingTimer) {
cancel(this.typingTimer);
}
if (this.displayedText.length === 0) {
element.innerHTML = "";
}
this._typeCharacter(element, text);
}
async _typeCharacter(element, text) {
if (this.displayedText.length < text.length) {
this.displayedText += text.charAt(this.displayedText.length);
try {
const cookedText = await cook(this.displayedText);
element.classList.add("cooked");
element.innerHTML = htmlSafe(cookedText);
} catch (error) {
console.error("Error cooking text during typing: ", error);
}
this.typingTimer = later(
this,
this._typeCharacter,
element,
text,
STREAMED_TEXT_SPEED
);
} else {
this.typingTimer = null;
}
}
willRemove() {
if (this.typingTimer) {
cancel(this.typingTimer);
}
}
}
@@ -1,18 +1,20 @@
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 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 { withSilencedDeprecations } from "discourse-common/lib/deprecated";
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
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 { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
let enabledChatBotIds = [];
let allowDebug = false;
function isGPTBot(user) {
return user && enabledChatBotIds.includes(user.id);
}
@@ -22,29 +24,7 @@ function attachHeaderIcon(api) {
}
function initializeAIBotReplies(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);
});
initializePauseButton(api);
api.modifyClass("controller:topic", {
pluginId: "discourse-ai",
@@ -102,7 +82,42 @@ function initializePersonaDecorator(api) {
);
}
const MAX_PERSONA_USER_ID = -1200;
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);
});
}
function initializeDebugButton(api) {
const currentUser = api.getCurrentUser();
@@ -110,10 +125,30 @@ 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");
modal.show(DebugAiModal, { model: post });
AiDebugButton.debugAiResponse(post, modal);
};
api.addPostMenuButton("debugAi", (post) => {
@@ -121,15 +156,8 @@ function initializeDebugButton(api) {
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;
}
if (!isPostFromAiBot(post, currentUser)) {
return;
}
return {
@@ -148,14 +176,29 @@ function initializeShareButton(api) {
return;
}
let shareAiResponse = async function ({ post, 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 });
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);
};
api.addPostMenuButton("share", (post) => {
@@ -164,21 +207,14 @@ function initializeShareButton(api) {
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;
}
if (!isPostFromAiBot(post, currentUser)) {
return;
}
return {
action: shareAiResponse,
icon: "far-copy",
className: "post-action-menu__share",
className: "post-action-menu__share-ai",
title: "discourse_ai.ai_bot.share",
position: "first",
};
@@ -218,10 +254,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.6.0", initializeAIBotReplies);
withPluginApi("1.34.0", initializeAIBotReplies);
withPluginApi("1.6.0", initializePersonaDecorator);
withPluginApi("1.22.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
withPluginApi("1.34.0", (api) => initializeDebugButton(api, container));
withPluginApi("1.34.0", (api) => initializeShareButton(api, container));
withPluginApi("1.22.0", (api) =>
initializeShareTopicButton(api, container)
);
@@ -1,6 +1,7 @@
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>
@@ -11,7 +12,7 @@ const RelatedTopics = <template>
class="more-topics__list"
>
<h3 id="related-topics-title" class="more-topics__list-title">
{{i18n "discourse_ai.related_topics.title"}}
{{icon "discourse-sparkles"}}{{i18n "discourse_ai.related_topics.title"}}
</h3>
<div class="topics">
<BasicTopicList @topics={{@topic.relatedTopics}} />
@@ -1,3 +1,15 @@
.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,22 +224,58 @@
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: 70ch;
max-width: 65ch;
.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,6 +333,8 @@ ar:
response_tokens: "رموز الرد:"
request: "طلب"
response: "الرد"
next_log: "التالي"
previous_log: "السابق"
share_full_topic_modal:
title: "مشاركة المحادثة بشكلٍ علني"
share: "مشاركة ونسخ الرابط"
+1
View File
@@ -77,5 +77,6 @@ be:
ai_bot:
debug_ai_modal:
request: "запыт"
next_log: "Далей"
share_modal:
copy: "капіяваць"
+2
View File
@@ -80,6 +80,8 @@ bg:
ai_bot:
debug_ai_modal:
request: "Заявка"
next_log: "Напред"
previous_log: "Предишни"
share_modal:
copy: "Копирай"
summarization:
+2
View File
@@ -88,5 +88,7 @@ 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,5 +89,7 @@ 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,6 +92,8 @@ 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,5 +91,7 @@ da:
debug_ai_modal:
request: "Anmod om medlemskab"
response: "Svar"
next_log: "Næste"
previous_log: "Forrige"
share_modal:
copy: "Kopier"
+7 -2
View File
@@ -11,7 +11,8 @@ de:
scopes:
descriptions:
discourse_ai:
search: "Ermöglicht die semantische Suche über den Endpunkt /discourse-ai/embeddings/semantic-search."
search: "Ermöglicht semantische Suche"
stream_completion: "Ermöglicht das Streamen von KI-Persona-Vervollständigungen"
site_settings:
categories:
discourse_ai: "Discourse-KI"
@@ -403,6 +404,8 @@ 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"
@@ -448,7 +451,9 @@ de:
topic:
title: "Zusammenfassung des Themas"
close: "Zusammenfassungspanel schließen"
disclosure: "Von KI erstellte Zusammenfassungen"
gists_enabled: "mit Zusammenfassung"
gists_enabled_long: "mit KI-generierter Zusammenfassung"
gists_disabled: "ohne Zusammenfassung"
review:
types:
reviewable_ai_post:
+2
View File
@@ -88,5 +88,7 @@ el:
debug_ai_modal:
request: "Αίτημα"
response: "Απάντηση"
next_log: "Επόμενο"
previous_log: "Προηγούμενο"
share_modal:
copy: "Αντιγραφή"
File diff suppressed because it is too large Load Diff

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