Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
943aa7468d |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,7 +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";
|
||||
import smoothStreamText from "../../modifiers/smooth-stream-text";
|
||||
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;
|
||||
@@ -35,7 +35,8 @@ export default class AiSummaryBox extends Component {
|
||||
@tracked canRegenerate = false;
|
||||
@tracked loading = false;
|
||||
@tracked isStreaming = false;
|
||||
@tracked haltAnimation = false;
|
||||
oldRaw = null; // used for comparison in SummaryUpdater in lib/ai-streamer/updaters
|
||||
finalSummary = null;
|
||||
|
||||
get outdatedSummaryWarningText() {
|
||||
let outdatedText = I18n.t("summary.outdated");
|
||||
@@ -82,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
|
||||
@@ -117,8 +119,6 @@ export default class AiSummaryBox extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// ensure summary is reset before requesting a new one:
|
||||
this.resetSummary();
|
||||
return this._requestSummary(fetchURL);
|
||||
}
|
||||
|
||||
@@ -134,32 +134,35 @@ 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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.text = newText;
|
||||
this.loading = false;
|
||||
|
||||
this.isStreaming = true;
|
||||
streamUpdaterText(SummaryUpdater, topicSummary, this);
|
||||
|
||||
if (update.done) {
|
||||
this.text = newText;
|
||||
this.isStreaming = false;
|
||||
this.haltAnimation = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -224,13 +227,8 @@ export default class AiSummaryBox extends Component {
|
||||
{{#if this.loading}}
|
||||
<AiSummarySkeleton />
|
||||
{{else}}
|
||||
<div
|
||||
class="generated-summary"
|
||||
{{smoothStreamText this.text this.haltAnimation}}
|
||||
>
|
||||
{{#if this.haltAnimation}}
|
||||
<CookText @rawText={{this.text}} />
|
||||
{{/if}}
|
||||
<div class="generated-summary cooked">
|
||||
{{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,60 +0,0 @@
|
||||
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,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)
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -333,8 +333,6 @@ ar:
|
||||
response_tokens: "رموز الرد:"
|
||||
request: "طلب"
|
||||
response: "الرد"
|
||||
next_log: "التالي"
|
||||
previous_log: "السابق"
|
||||
share_full_topic_modal:
|
||||
title: "مشاركة المحادثة بشكلٍ علني"
|
||||
share: "مشاركة ونسخ الرابط"
|
||||
|
||||
@@ -77,6 +77,5 @@ be:
|
||||
ai_bot:
|
||||
debug_ai_modal:
|
||||
request: "запыт"
|
||||
next_log: "Далей"
|
||||
share_modal:
|
||||
copy: "капіяваць"
|
||||
|
||||
@@ -80,8 +80,6 @@ bg:
|
||||
ai_bot:
|
||||
debug_ai_modal:
|
||||
request: "Заявка"
|
||||
next_log: "Напред"
|
||||
previous_log: "Предишни"
|
||||
share_modal:
|
||||
copy: "Копирай"
|
||||
summarization:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -88,7 +88,5 @@ 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
Reference in New Issue
Block a user