Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ce8d943a7 | ||
|
|
9551b1a4d1 | ||
|
|
aef9a03d4c | ||
|
|
9583964676 | ||
|
|
2fc05685bb | ||
|
|
26bfda4763 | ||
|
|
6ef635619f | ||
|
|
e817b7dc11 | ||
|
|
644141ff08 | ||
|
|
fbc74c7467 | ||
|
|
c421f713a3 | ||
|
|
021e09607d | ||
|
|
893fa624e4 | ||
|
|
1ad5321c09 | ||
|
|
da7c97d294 | ||
|
|
ab4678275d | ||
|
|
99282612a9 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
+20
-18
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -333,6 +333,8 @@ ar:
|
||||
response_tokens: "رموز الرد:"
|
||||
request: "طلب"
|
||||
response: "الرد"
|
||||
next_log: "التالي"
|
||||
previous_log: "السابق"
|
||||
share_full_topic_modal:
|
||||
title: "مشاركة المحادثة بشكلٍ علني"
|
||||
share: "مشاركة ونسخ الرابط"
|
||||
|
||||
@@ -77,5 +77,6 @@ be:
|
||||
ai_bot:
|
||||
debug_ai_modal:
|
||||
request: "запыт"
|
||||
next_log: "Далей"
|
||||
share_modal:
|
||||
copy: "капіяваць"
|
||||
|
||||
@@ -80,6 +80,8 @@ bg:
|
||||
ai_bot:
|
||||
debug_ai_modal:
|
||||
request: "Заявка"
|
||||
next_log: "Напред"
|
||||
previous_log: "Предишни"
|
||||
share_modal:
|
||||
copy: "Копирай"
|
||||
summarization:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -333,6 +333,8 @@ es:
|
||||
response_tokens: "Tokens de respuesta:"
|
||||
request: "Solicitud"
|
||||
response: "Respuesta"
|
||||
next_log: "Siguiente"
|
||||
previous_log: "Anterior"
|
||||
share_full_topic_modal:
|
||||
title: "Compartir la conversación públicamente"
|
||||
share: "Compartir y copiar enlace"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user