Compare commits

..
Author SHA1 Message Date
Tobias Eigen 655c7e9708 fixed "fools" to "tools" typo in client.en.yml 2024-11-18 10:59:20 -08:00
Rafael dos Santos Silva 4fb686a548 FIX: Move emotion /filter logic into a CTE to keep cardinality sane (#915) 2024-11-14 17:16:48 -03:00
Rafael dos Santos Silva 5026ab52d0 FEATURE: Order by emotion on /filter (#913) 2024-11-14 12:45:40 -03:00
Sam 823e8ef490 FEATURE: partial tool call support for OpenAI and Anthropic (#908)
Implement streaming tool call implementation for Anthropic and Open AI.

When calling:

llm.generate(..., partial_tool_calls: true) do ...
Partials may contain ToolCall instances with partial: true, These tool calls are partially populated with json partially parsed.

So for example when performing a search you may get:

ToolCall(..., {search: "hello" })
ToolCall(..., {search: "hello world" })

The library used to parse json is:

https://github.com/dgraham/json-stream

We use a fork cause we need access to the internal buffer.

This prepares internals to perform partial tool calls, but does not implement it yet.
2024-11-14 06:58:24 +11:00
Keegan George f75b13c4fa FIX: results not being reset when appending to query param (#912)
This PR fixes an issue where the AI search results were not being reset when you append your search to an existing query param (typically when you've come from quick search). This is because `handleSearch()` doesn't get called in this situation. So here we explicitly check for query params, trigger a reset and search for those occasions.
2024-11-13 07:19:34 -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
110 changed files with 4217 additions and 1775 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)
+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
@@ -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,179 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import { SEARCH_TYPE_DEFAULT } from "discourse/controllers/full-page-search";
import { ajax } from "discourse/lib/ajax";
import { withPluginApi } from "discourse/lib/plugin-api";
import { isValidSearchTerm, translateResults } from "discourse/lib/search";
import icon from "discourse-common/helpers/d-icon";
import I18n from "I18n";
import AiIndicatorWave from "./ai-indicator-wave";
export default class AiFullPageSemanticSearch extends Component {
@service appEvents;
@service siteSettings;
@service searchPreferencesManager;
@tracked searching = false;
@tracked AIResults = [];
@tracked showingAIResults = false;
initialSearchTerm = this.args.outletArgs.search;
get disableToggleSwitch() {
if (
this.searching ||
this.AIResults.length === 0 ||
this.args.outletArgs.sortOrder !== 0
) {
return true;
}
}
get searchStateText() {
// Search results:
if (this.AIResults.length > 0) {
if (this.showingAIResults) {
return I18n.t(
"discourse_ai.embeddings.semantic_search_results.toggle",
{
count: this.AIResults.length,
}
);
} else {
return I18n.t(
"discourse_ai.embeddings.semantic_search_results.toggle_hidden",
{
count: this.AIResults.length,
}
);
}
}
// Search loading:
if (this.searching) {
return I18n.t("discourse_ai.embeddings.semantic_search_loading");
}
// Typing to search:
if (
this.AIResults.length === 0 &&
this.searchTerm !== this.initialSearchTerm
) {
return I18n.t("discourse_ai.embeddings.semantic_search_results.new");
}
// No results:
if (this.AIResults.length === 0) {
return I18n.t("discourse_ai.embeddings.semantic_search_results.none");
}
}
get searchTerm() {
if (this.initialSearchTerm !== this.args.outletArgs.search) {
this.initialSearchTerm = undefined;
}
return this.args.outletArgs.search;
}
get searchEnabled() {
return (
this.args.outletArgs.type === SEARCH_TYPE_DEFAULT &&
isValidSearchTerm(this.searchTerm, this.siteSettings) &&
this.args.outletArgs.sortOrder === 0
);
}
@action
toggleAIResults() {
if (this.showingAIResults) {
this.args.outletArgs.addSearchResults([], "topic_id");
} else {
this.args.outletArgs.addSearchResults(this.AIResults, "topic_id");
}
this.showingAIResults = !this.showingAIResults;
}
@action
resetAIResults() {
this.AIResults = [];
this.showingAIResults = false;
this.args.outletArgs.addSearchResults([], "topic_id");
}
@action
handleSearch() {
if (!this.searchEnabled) {
return;
}
if (this.initialSearchTerm && !this.searching) {
return this.performHyDESearch();
}
withPluginApi("1.15.0", (api) => {
api.onAppEvent("full-page-search:trigger-search", () => {
if (!this.searching) {
this.resetAIResults();
return this.performHyDESearch();
}
});
});
}
performHyDESearch() {
this.searching = true;
this.resetAIResults();
ajax("/discourse-ai/embeddings/semantic-search", {
data: { q: this.searchTerm },
})
.then(async (results) => {
const model = (await translateResults(results)) || {};
if (model.posts?.length === 0) {
this.searching = false;
return;
}
model.posts.forEach((post) => {
post.generatedByAI = true;
});
this.AIResults = model.posts;
})
.finally(() => (this.searching = false));
}
<template>
{{#if this.searchEnabled}}
<div class="semantic-search__container search-results" role="region">
<div class="semantic-search__results" {{didInsert this.handleSearch}}>
<div
class="semantic-search__searching
{{if this.searching 'in-progress'}}"
>
<DToggleSwitch
disabled={{this.disableToggleSwitch}}
@state={{this.showingAIResults}}
title="AI search results hidden"
class="semantic-search__results-toggle"
{{on "click" this.toggleAIResults}}
/>
<div class="semantic-search__searching-text">
{{icon "discourse-sparkles"}}
{{this.searchStateText}}
</div>
<AiIndicatorWave @loading={{this.searching}} />
</div>
</div>
</div>
{{/if}}
</template>
}
@@ -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>
}
@@ -1,12 +1,198 @@
import Component from "@glimmer/component";
import AIFullPageSemanticSearch from "discourse/plugins/discourse-ai/discourse/components/ai-full-page-semantic-search";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import { SEARCH_TYPE_DEFAULT } from "discourse/controllers/full-page-search";
import { ajax } from "discourse/lib/ajax";
import { withPluginApi } from "discourse/lib/plugin-api";
import { isValidSearchTerm, translateResults } from "discourse/lib/search";
import icon from "discourse-common/helpers/d-icon";
import I18n from "I18n";
import AiIndicatorWave from "../../components/ai-indicator-wave";
export default class SemanticSearch extends Component {
static shouldRender(_args, { siteSettings }) {
return siteSettings.ai_embeddings_semantic_search_enabled;
}
@service router;
@service appEvents;
@service siteSettings;
@service searchPreferencesManager;
@tracked searching = false;
@tracked AiResults = [];
@tracked showingAiResults = false;
initialSearchTerm = this.args.outletArgs.search;
get disableToggleSwitch() {
if (
this.searching ||
this.AiResults.length === 0 ||
this.args.outletArgs.sortOrder !== 0
) {
return true;
}
}
get searchStateText() {
// Search results:
if (this.AiResults.length > 0) {
if (this.showingAiResults) {
return I18n.t(
"discourse_ai.embeddings.semantic_search_results.toggle",
{
count: this.AiResults.length,
}
);
} else {
return I18n.t(
"discourse_ai.embeddings.semantic_search_results.toggle_hidden",
{
count: this.AiResults.length,
}
);
}
}
// Search loading:
if (this.searching) {
return I18n.t("discourse_ai.embeddings.semantic_search_loading");
}
// Typing to search:
if (
this.AiResults.length === 0 &&
this.searchTerm !== this.initialSearchTerm
) {
return I18n.t("discourse_ai.embeddings.semantic_search_results.new");
}
// No results:
if (this.AiResults.length === 0) {
return I18n.t("discourse_ai.embeddings.semantic_search_results.none");
}
}
get searchTerm() {
if (this.initialSearchTerm !== this.args.outletArgs.search) {
this.initialSearchTerm = undefined;
}
return this.args.outletArgs.search;
}
get searchEnabled() {
return (
this.args.outletArgs.type === SEARCH_TYPE_DEFAULT &&
isValidSearchTerm(this.searchTerm, this.siteSettings) &&
this.args.outletArgs.sortOrder === 0
);
}
@action
toggleAiResults() {
if (this.showingAiResults) {
this.args.outletArgs.addSearchResults([], "topic_id");
} else {
this.args.outletArgs.addSearchResults(this.AiResults, "topic_id");
}
this.showingAiResults = !this.showingAiResults;
}
@action
resetAiResults() {
this.AiResults = [];
this.showingAiResults = false;
this.args.outletArgs.addSearchResults([], "topic_id");
}
@action
handleSearch() {
if (!this.searchEnabled) {
return;
}
if (this.initialSearchTerm && !this.searching) {
return this.performHyDESearch();
}
this.#resetAndSearchOnEvent();
}
performHyDESearch() {
this.searching = true;
this.resetAiResults();
ajax("/discourse-ai/embeddings/semantic-search", {
data: { q: this.searchTerm },
})
.then(async (results) => {
const model = (await translateResults(results)) || {};
if (model.posts?.length === 0) {
this.searching = false;
return;
}
model.posts.forEach((post) => {
post.generatedByAi = true;
});
this.AiResults = model.posts;
})
.finally(() => (this.searching = false));
}
#resetAndSearchOnEvent() {
return withPluginApi("1.15.0", (api) => {
api.onAppEvent("full-page-search:trigger-search", () => {
if (!this.searching) {
this.resetAiResults();
return this.performHyDESearch();
}
});
});
}
@action
checkQueryParamsAndSearch() {
// This check is necessary because handleSearch() isn't called
// if query params are present and a new search has appended text.
// It ensures AiResults are reset and searched for properly
const searchQueryParam = this.router.currentRoute?.queryParams?.q;
if (searchQueryParam) {
this.#resetAndSearchOnEvent();
}
}
<template>
<AIFullPageSemanticSearch @outletArgs={{@outletArgs}} />
<span {{didInsert this.checkQueryParamsAndSearch}}></span>
{{#if this.searchEnabled}}
<div class="semantic-search__container search-results" role="region">
<div class="semantic-search__results" {{didInsert this.handleSearch}}>
<div
class="semantic-search__searching
{{if this.searching 'in-progress'}}"
>
<DToggleSwitch
disabled={{this.disableToggleSwitch}}
@state={{this.showingAiResults}}
class="semantic-search__results-toggle"
{{on "click" this.toggleAiResults}}
/>
<div class="semantic-search__searching-text">
{{icon "discourse-sparkles"}}
{{this.searchStateText}}
</div>
<AiIndicatorWave @loading={{this.searching}} />
</div>
</div>
</div>
{{/if}}
</template>
}
@@ -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,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)
);
@@ -4,6 +4,6 @@ export default apiInitializer("1.15.0", (api) => {
api.modifyClass("component:search-result-entry", {
pluginId: "discourse-ai",
classNameBindings: ["bulkSelectEnabled", "post.generatedByAI:ai-result"],
classNameBindings: ["bulkSelectEnabled", "post.generatedByAi:ai-result"],
});
});
@@ -240,14 +240,10 @@
.--topic-list-with-gist {
.topic-list-item {
.main-link {
.desktop-view & {
padding: 1em 1em 1em 0.67em;
}
font-size: var(--font-up-2);
line-height: var(--line-height-medium);
}
.link-bottom-line {
font-size: var(--font-down-2);
font-size: var(--font-down-1);
margin-top: 0.25em;
}
.ai-topic-gist {
+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"
+2
View File
@@ -404,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"
+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
+2
View File
@@ -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"
+2
View File
@@ -83,5 +83,7 @@ et:
debug_ai_modal:
request: "Päring"
response: "Vastus"
next_log: "Järgmine"
previous_log: "Eelmine"
share_modal:
copy: "Kopeeri"
+2
View File
@@ -132,6 +132,8 @@ fa_IR:
debug_ai_modal:
request: "درخواست"
response: "پاسخ"
next_log: "بعدی"
previous_log: "پیشین"
ai_label: "هوش مصنوعی"
share_modal:
copy: "کپی"
+2
View File
@@ -333,6 +333,8 @@ fi:
response_tokens: "Vastauksen tokenit:"
request: "Pyyntö"
response: "Vastaus"
next_log: "Seuraava"
previous_log: "Edellinen"
share_full_topic_modal:
title: "Jaa keskustelu julkisesti"
share: "Jaa ja kopioi linkki"
+2
View File
@@ -333,6 +333,8 @@ fr:
response_tokens: "Jetons de réponse :"
request: "Requête"
response: "Réponse"
next_log: "Suivant"
previous_log: "Précédent"
share_full_topic_modal:
title: "Partager la conversation publiquement"
share: "Partager et copier le lien"
+2
View File
@@ -89,5 +89,7 @@ gl:
debug_ai_modal:
request: "Petición"
response: "Resposta"
next_log: "Seguinte"
previous_log: "Anterior"
share_modal:
copy: "Copiar"
+5
View File
@@ -157,6 +157,8 @@ he:
forced_tool_strategy: אסטרטגיית כלי כפוי
allow_chat_direct_messages: "לאפשר הודעות ישירות בצ׳אט"
allow_chat_direct_messages_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לשלוח הודעות ישירות לדמות הזאת."
allow_chat_channel_mentions: "לאפשר אזכורים בערוצי צ׳אט"
allow_chat_channel_mentions_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לאזכר את הדמות הזאת בערוצי צ׳אט."
allow_personal_messages: "לאפשר הודעות פרטיות"
allow_personal_messages_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לשלוח הודעות פרטיות לדמות הזאת."
allow_topic_mentions: "לאפשר אזכורים בנושאים"
@@ -299,6 +301,7 @@ he:
max_prompt_tokens: "מספר האסימונים המרבי לבקשה. ככלל אצבע, זה אמור להיות 50% מחלון ההקשר של המודל."
name: "אנו מצרפים את זה לקריאת ה־API כדי לציין באיזה מודל להשתמש."
vision_enabled: "אם האפשרות פעילה, הבינה מלאכותית תנסה להבין תמונות. כתלות בתמיכה של המודל בעיבוד תמונות. נתמך על ידי המודלים העדכניים ביותר מבית Anthropic,‏ Google ו־OpenAI."
enabled_chat_bot: "אם האפשרות פעילה, משתמשים יכולים לבחור את המודל הזה בעת יצירת הודעות פרטיות עם בוט הבינה המלאכותית."
providers:
aws_bedrock: "AWS Bedrock"
anthropic: "Anthropic"
@@ -399,6 +402,8 @@ he:
response_tokens: "אסימוני תגובה:"
request: "בקשה"
response: "תשובה"
next_log: "הבא"
previous_log: "הקודם"
share_full_topic_modal:
title: "שיתוף שיחה באופן ציבורי"
share: "שיתוף והעתקת קישור"

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