Compare commits

..
52 changed files with 425 additions and 1312 deletions
@@ -4,9 +4,7 @@ export default DiscourseRoute.extend({
async model(params) {
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const id = parseInt(params.id, 10);
const record = allLlms.findBy("id", id);
record.provider_params = record.provider_params || {};
return record;
return allLlms.findBy("id", id);
},
setupController(controller, model) {
@@ -45,10 +45,6 @@ module DiscourseAi
def update
llm_model = LlmModel.find(params[:id])
if llm_model.seeded?
return render_json_error(I18n.t("discourse_ai.llm.cannot_edit_builtin"), status: 403)
end
if llm_model.update(ai_llm_params(updating: llm_model))
llm_model.toggle_companion_user
render json: LlmModelSerializer.new(llm_model)
@@ -60,10 +56,6 @@ module DiscourseAi
def destroy
llm_model = LlmModel.find(params[:id])
if llm_model.seeded?
return render_json_error(I18n.t("discourse_ai.llm.cannot_delete_builtin"), status: 403)
end
in_use_by = DiscourseAi::Configuration::LlmValidator.new.modules_using(llm_model)
if !in_use_by.empty?
@@ -125,21 +117,11 @@ module DiscourseAi
new_url = params.dig(:ai_llm, :url)
permitted[:url] = new_url if permit_url && new_url
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym)
if extra_field_names.present?
received_prov_params =
params.dig(:ai_llm, :provider_params)&.slice(*extra_field_names.keys)
if received_prov_params.present?
received_prov_params.each do |pname, value|
if extra_field_names[pname.to_sym] == :checkbox
received_prov_params[pname] = ActiveModel::Type::Boolean.new.cast(value)
end
end
permitted[:provider_params] = received_prov_params.permit!
end
end
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym, :fields).to_a
received_prov_params = params.dig(:ai_llm, :provider_params)
permitted[:provider_params] = received_prov_params.slice(
*extra_field_names,
).permit! if !extra_field_names.empty? && received_prov_params.present?
permitted
end
@@ -9,7 +9,6 @@ module DiscourseAi
def search
query = params[:q].to_s
skip_hyde = params[:hyde].downcase.to_s == "false" || params[:hyde].to_s == "0"
if query.length < SiteSetting.min_search_term_length
raise Discourse::InvalidParameters.new(:q)
@@ -32,7 +31,7 @@ module DiscourseAi
hijack do
semantic_search
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
.search_for_topics(query)
.each { |topic_post| grouped_results.add(topic_post) }
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
@@ -40,9 +39,6 @@ module DiscourseAi
end
def quick_search
# this search function searches posts (vs: topics)
# it requires post embeddings and a reranker
# it will not perform a hyde expantion
query = params[:q].to_s
if query.length < SiteSetting.min_search_term_length
@@ -12,13 +12,6 @@ module ::Jobs
return unless post.topic.custom_fields[DiscourseAi::AiBot::EntryPoint::REQUIRE_TITLE_UPDATE]
DiscourseAi::AiBot::Playground.new(bot).title_playground(post)
publish_update(post.topic, { title: post.topic.title })
end
def publish_update(topic, payload)
allowed_users = topic.topic_allowed_users.pluck(:user_id)
MessageBus.publish("/discourse-ai/ai-bot/topic/#{topic.id}", payload, user_ids: allowed_users)
end
end
end
+4 -16
View File
@@ -17,20 +17,12 @@ class LlmModel < ActiveRecord::Base
def self.provider_params
{
aws_bedrock: {
access_key_id: :text,
region: :text,
url_editable: false,
fields: %i[access_key_id region],
},
open_ai: {
organization: :text,
},
hugging_face: {
disable_system_prompt: :checkbox,
},
vllm: {
disable_system_prompt: :checkbox,
},
ollama: {
disable_system_prompt: :checkbox,
url_editable: true,
fields: %i[organization],
},
}
end
@@ -89,10 +81,6 @@ class LlmModel < ActiveRecord::Base
provider_params&.dig(key)
end
def seeded?
id < 0
end
private
def required_provider_params
-12
View File
@@ -21,16 +21,4 @@ class LlmModelSerializer < ApplicationSerializer
def used_by
DiscourseAi::Configuration::LlmValidator.new.modules_using(object)
end
def api_key
object.seeded? ? "********" : object.api_key
end
def url
object.seeded? ? "********" : object.url
end
def provider
object.seeded? ? "CDCK" : object.provider
end
end
@@ -20,25 +20,26 @@ export default class AiTool extends RestModel {
return this.getProperties(CREATE_ATTRIBUTES);
}
trackParameters(parameters) {
return new TrackedArray(
parameters?.map((p) => {
workingCopy() {
const attrs = this.getProperties(CREATE_ATTRIBUTES);
attrs.parameters = new TrackedArray(
attrs.parameters?.map((p) => {
const parameter = new TrackedObject(p);
if (parameter.enum && parameter.enum.length) {
parameter.enum = new TrackedArray(parameter.enum);
//Backwards-compatibility code.
// TODO(roman): Remove aug 2024. Leave only else clause.
if (parameter.enum_values) {
parameter.enum = new TrackedArray(parameter.enum_values);
delete parameter.enum_values;
} else {
parameter.enum = null;
parameter.enum = new TrackedArray(parameter.enum);
}
return parameter;
})
);
}
workingCopy() {
const attrs = this.getProperties(CREATE_ATTRIBUTES);
attrs.parameters = this.trackParameters(attrs.parameters);
return this.store.createRecord("ai-tool", attrs);
}
}
@@ -1,5 +1,6 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { modifier } from "ember-modifier";
@@ -16,7 +17,6 @@ import ModalDiffModal from "../components/modal/diff-modal";
import ThumbnailSuggestion from "../components/modal/thumbnail-suggestions";
export default class AiComposerHelperMenu extends Component {
@service modal;
@service siteSettings;
@service aiComposerHelper;
@service currentUser;
@@ -29,6 +29,7 @@ export default class AiComposerHelperMenu extends Component {
@tracked lastUsedOption = null;
@tracked thumbnailSuggestions = null;
@tracked showThumbnailModal = false;
@tracked showDiffModal = false;
@tracked lastSelectionRange = null;
MENU_STATES = this.aiComposerHelper.MENU_STATES;
prompts = [];
@@ -99,16 +100,7 @@ export default class AiComposerHelperMenu extends Component {
{
icon: "exchange-alt",
label: "discourse_ai.ai_helper.context_menu.view_changes",
action: () =>
this.modal.show(ModalDiffModal, {
model: {
diff: this.diff,
oldValue: this.initialValue,
newValue: this.newSelectedText,
revert: this.undoAiAction,
confirm: () => this.updateMenuState(this.MENU_STATES.resets),
},
}),
action: () => (this.showDiffModal = true),
classes: "view-changes",
},
{
@@ -162,14 +154,6 @@ export default class AiComposerHelperMenu extends Component {
return true;
}
get isExpanded() {
if (this.aiComposerHelper.menuState === this.MENU_STATES.triggers) {
return "";
}
return "is-expanded";
}
@bind
onKeyDown(event) {
if (event.key === "Escape") {
@@ -218,12 +202,8 @@ export default class AiComposerHelperMenu extends Component {
if (option.name === "illustrate_post") {
this._toggleLoadingState(false);
this.closeMenu();
this.showThumbnailModal = true;
this.thumbnailSuggestions = data.thumbnails;
this.modal.show(ThumbnailSuggestion, {
model: {
thumbnails: this.thumbnailSuggestions,
},
});
} else {
this._updateSuggestedByAi(data);
}
@@ -314,10 +294,7 @@ export default class AiComposerHelperMenu extends Component {
}
<template>
<div
class="ai-composer-helper-menu {{this.isExpanded}}"
{{this.documentListeners}}
>
<div class="ai-composer-helper-menu" {{this.documentListeners}}>
{{#if (eq this.aiComposerHelper.menuState this.MENU_STATES.triggers)}}
<ul class="ai-composer-helper-menu__triggers">
<li>
@@ -349,5 +326,26 @@ export default class AiComposerHelperMenu extends Component {
/>
{{/if}}
</div>
{{#if this.showDiffModal}}
<ModalDiffModal
@confirm={{fn
(mut this.aiComposerHelper.menuState)
this.MENU_STATES.resets
}}
@diff={{this.diff}}
@oldValue={{this.initialValue}}
@newValue={{this.newSelectedText}}
@revert={{this.undoAiAction}}
@closeModal={{fn (mut this.showDiffModal) false}}
/>
{{/if}}
{{#if this.showThumbnailModal}}
<ThumbnailSuggestion
@thumbnails={{this.thumbnailSuggestions}}
@closeModal={{fn (mut this.showThumbnailModal) false}}
/>
{{/if}}
</template>
}
@@ -7,7 +7,6 @@ import { action, computed } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { eq } from "truth-helpers";
import DButton from "discourse/components/d-button";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import Avatar from "discourse/helpers/bound-avatar-template";
@@ -53,19 +52,15 @@ export default class AiLlmEditorForm extends Component {
return this.testRunning || this.testResult !== null;
}
@computed("args.model.provider")
get canEditURL() {
return this.args.model.provider !== "aws_bedrock";
// Explicitly false.
return this.metaProviderParams.url_editable !== false;
}
get modulesUsingModel() {
return this.args.model.used_by?.join(", ");
}
get seeded() {
return this.args.model.id < 0;
}
get inUseWarning() {
return I18n.t("discourse_ai.llms.in_use_warning", {
settings: this.modulesUsingModel,
@@ -174,19 +169,13 @@ export default class AiLlmEditorForm extends Component {
}
<template>
{{#if this.seeded}}
<div class="alert alert-info">
{{icon "exclamation-circle"}}
{{i18n "discourse_ai.llms.seeded_warning"}}
</div>
{{/if}}
{{#if this.modulesUsingModel}}
<div class="alert alert-info">
{{icon "exclamation-circle"}}
{{this.inUseWarning}}
</div>
{{/if}}
<form class="form-horizontal ai-llm-editor {{if this.seeded 'seeded'}}">
<form class="form-horizontal ai-llm-editor">
<div class="control-group">
<label>{{i18n "discourse_ai.llms.display_name"}}</label>
<Input
@@ -212,136 +201,123 @@ export default class AiLlmEditorForm extends Component {
<ComboBox
@value={{@model.provider}}
@content={{this.selectedProviders}}
@class="ai-llm-editor__provider"
/>
</div>
{{#unless this.seeded}}
{{#if this.canEditURL}}
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.url"}}</label>
<Input
class="ai-llm-editor-input ai-llm-editor__url"
@type="text"
@value={{@model.url}}
required="true"
/>
</div>
{{/if}}
{{#if this.canEditURL}}
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.api_key"}}</label>
<div class="ai-llm-editor__secret-api-key-group">
<Input
@value={{@model.api_key}}
class="ai-llm-editor-input ai-llm-editor__api-key"
@type={{if this.apiKeySecret "password" "text"}}
required="true"
{{on "focusout" this.makeApiKeySecret}}
/>
<DButton
@action={{this.toggleApiKeySecret}}
@icon="far-eye-slash"
/>
</div>
</div>
{{#each-in this.metaProviderParams as |field type|}}
<div class="control-group ai-llm-editor-provider-param__{{type}}">
<label>{{I18n.t
(concat "discourse_ai.llms.provider_fields." field)
}}</label>
{{#if (eq type "checkbox")}}
<Input
@type={{type}}
@checked={{mut (get @model.provider_params field)}}
/>
{{else}}
<Input
@type={{type}}
@value={{mut (get @model.provider_params field)}}
/>
{{/if}}
</div>
{{/each-in}}
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.tokenizer"}}</label>
<ComboBox
@value={{@model.tokenizer}}
@content={{@llms.resultSetMeta.tokenizers}}
@class="ai-llm-editor__tokenizer"
/>
</div>
<div class="control-group">
<label>{{i18n "discourse_ai.llms.max_prompt_tokens"}}</label>
<label>{{I18n.t "discourse_ai.llms.url"}}</label>
<Input
@type="number"
class="ai-llm-editor-input ai-llm-editor__max-prompt-tokens"
step="any"
min="0"
lang="en"
@value={{@model.max_prompt_tokens}}
class="ai-llm-editor-input ai-llm-editor__url"
@type="text"
@value={{@model.url}}
required="true"
/>
<DTooltip
@icon="question-circle"
@content={{I18n.t "discourse_ai.llms.hints.max_prompt_tokens"}}
/>
</div>
<div class="control-group ai-llm-editor__vision-enabled">
<Input @type="checkbox" @checked={{@model.vision_enabled}} />
<label>{{I18n.t "discourse_ai.llms.vision_enabled"}}</label>
<DTooltip
@icon="question-circle"
@content={{I18n.t "discourse_ai.llms.hints.vision_enabled"}}
{{/if}}
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.api_key"}}</label>
<div class="ai-llm-editor__secret-api-key-group">
<Input
@value={{@model.api_key}}
class="ai-llm-editor-input ai-llm-editor__api-key"
@type={{if this.apiKeySecret "password" "text"}}
required="true"
{{on "focusout" this.makeApiKeySecret}}
/>
<DButton @action={{this.toggleApiKeySecret}} @icon="far-eye-slash" />
</div>
</div>
{{#each this.metaProviderParams.fields as |field|}}
<div class="control-group">
<DToggleSwitch
class="ai-llm-editor__enabled-chat-bot"
@state={{@model.enabled_chat_bot}}
@label="discourse_ai.llms.enabled_chat_bot"
{{on "click" this.toggleEnabledChatBot}}
<label>{{I18n.t
(concat "discourse_ai.llms.provider_fields." field)
}}</label>
<Input
@type="text"
@value={{mut (get @model.provider_params field)}}
class="ai-llm-editor-input ai-llm-editor__{{field}}"
/>
</div>
{{#if @model.user}}
<div class="control-group">
<label>{{i18n "discourse_ai.llms.ai_bot_user"}}</label>
<a
class="avatar"
href={{@model.user.path}}
data-user-card={{@model.user.username}}
>
{{Avatar @model.user.avatar_template "small"}}
</a>
<LinkTo @route="adminUser" @model={{this.adminUser}}>
{{@model.user.username}}
</LinkTo>
</div>
{{/if}}
<div class="control-group ai-llm-editor__action_panel">
<DButton
class="ai-llm-editor__test"
@action={{this.test}}
@disabled={{this.testRunning}}
{{/each}}
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.tokenizer"}}</label>
<ComboBox
@value={{@model.tokenizer}}
@content={{@llms.resultSetMeta.tokenizers}}
/>
</div>
<div class="control-group">
<label>{{i18n "discourse_ai.llms.max_prompt_tokens"}}</label>
<Input
@type="number"
class="ai-llm-editor-input ai-llm-editor__max-prompt-tokens"
step="any"
min="0"
lang="en"
@value={{@model.max_prompt_tokens}}
required="true"
/>
<DTooltip
@icon="question-circle"
@content={{I18n.t "discourse_ai.llms.hints.max_prompt_tokens"}}
/>
</div>
<div class="control-group ai-llm-editor__vision-enabled">
<Input @type="checkbox" @checked={{@model.vision_enabled}} />
<label>{{I18n.t "discourse_ai.llms.vision_enabled"}}</label>
<DTooltip
@icon="question-circle"
@content={{I18n.t "discourse_ai.llms.hints.vision_enabled"}}
/>
</div>
<div class="control-group">
<DToggleSwitch
class="ai-llm-editor__enabled-chat-bot"
@state={{@model.enabled_chat_bot}}
@label="discourse_ai.llms.enabled_chat_bot"
{{on "click" this.toggleEnabledChatBot}}
/>
</div>
{{#if @model.user}}
<div class="control-group">
<label>{{i18n "discourse_ai.llms.ai_bot_user"}}</label>
<a
class="avatar"
href={{@model.user.path}}
data-user-card={{@model.user.username}}
>
{{I18n.t "discourse_ai.llms.tests.title"}}
</DButton>
<DButton
class="btn-primary ai-llm-editor__save"
@action={{this.save}}
@disabled={{this.isSaving}}
>
{{I18n.t "discourse_ai.llms.save"}}
</DButton>
{{#unless @model.isNew}}
<DButton
@action={{this.delete}}
class="btn-danger ai-llm-editor__delete"
>
{{I18n.t "discourse_ai.llms.delete"}}
</DButton>
{{/unless}}
{{Avatar @model.user.avatar_template "small"}}
</a>
<LinkTo @route="adminUser" @model={{this.adminUser}}>
{{@model.user.username}}
</LinkTo>
</div>
{{/unless}}
{{/if}}
<div class="control-group ai-llm-editor__action_panel">
<DButton
class="ai-llm-editor__test"
@action={{this.test}}
@disabled={{this.testRunning}}
>
{{I18n.t "discourse_ai.llms.tests.title"}}
</DButton>
<DButton
class="btn-primary ai-llm-editor__save"
@action={{this.save}}
@disabled={{this.isSaving}}
>
{{I18n.t "discourse_ai.llms.save"}}
</DButton>
{{#unless @model.isNew}}
<DButton
@action={{this.delete}}
class="btn-danger ai-llm-editor__delete"
>
{{I18n.t "discourse_ai.llms.delete"}}
</DButton>
{{/unless}}
</div>
<div class="control-group ai-llm-editor-tests">
{{#if this.displayTestResult}}
@@ -25,7 +25,6 @@ export default class AiToolEditor extends Component {
@service dialog;
@service modal;
@service toasts;
@service store;
@tracked isSaving = false;
@tracked editingModel = null;
@@ -54,9 +53,8 @@ export default class AiToolEditor extends Component {
@action
configurePreset() {
this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId);
this.editingModel = this.store
.createRecord("ai-tool", this.selectedPreset)
.workingCopy();
this.editingModel = this.args.model.workingCopy();
this.editingModel.setProperties(this.selectedPreset);
this.showDelete = false;
}
@@ -194,7 +192,7 @@ export default class AiToolEditor extends Component {
<label>{{I18n.t "discourse_ai.tools.script"}}</label>
<AceEditor
@content={{this.editingModel.script}}
@onChange={{fn (mut this.editingModel.script)}}
@onChange={{withEventValue (fn (mut this.editingModel.script))}}
@mode={{ACE_EDITOR_MODE}}
@theme={{ACE_EDITOR_THEME}}
@editorId="ai-tool-script-editor"
@@ -7,6 +7,7 @@ import DButton from "discourse/components/d-button";
import withEventValue from "discourse/helpers/with-event-value";
import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
import and from "truth-helpers/helpers/and";
const PARAMETER_TYPES = [
{ name: "string", id: "string" },
@@ -58,9 +59,6 @@ export default class AiToolParameterEditor extends Component {
@action
removeEnumValue(parameter, index) {
parameter.enum.splice(index, 1);
if (parameter.enum.length === 0) {
parameter.enum = null;
}
}
@action
@@ -96,7 +94,6 @@ export default class AiToolParameterEditor extends Component {
{{on "input" (fn this.toggleRequired parameter)}}
checked={{parameter.required}}
type="checkbox"
class="parameter-row__required-toggle"
/>
{{I18n.t "discourse_ai.tools.parameter_required"}}
</label>
@@ -104,9 +101,8 @@ export default class AiToolParameterEditor extends Component {
<label>
<input
{{on "input" (fn this.toggleEnum parameter)}}
checked={{parameter.enum}}
checked={{and parameter.enum parameter.enum.length}}
type="checkbox"
class="parameter-row__enum-toggle"
/>
{{I18n.t "discourse_ai.tools.parameter_enum"}}
</label>
@@ -118,7 +114,7 @@ export default class AiToolParameterEditor extends Component {
/>
</div>
{{#if parameter.enum}}
{{#if (and parameter.enum parameter.enum.length)}}
<div class="parameter-enum-values">
{{#each parameter.enum as |enumValue enumIndex|}}
<div class="enum-value-row">
@@ -1,86 +1,21 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import CookText from "discourse/components/cook-text";
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 i18n from "discourse-common/helpers/i18n";
export default class ModalDiffModal extends Component {
@service currentUser;
@tracked loading = false;
@tracked diff;
suggestion = "";
PROOFREAD_ID = -303;
constructor() {
super(...arguments);
this.diff = this.args.model.diff;
next(() => {
if (this.args.model.toolbarEvent) {
this.loadDiff();
}
});
}
async loadDiff() {
this.loading = true;
try {
const suggestion = await ajax("/discourse-ai/ai-helper/suggest", {
method: "POST",
data: {
mode: this.PROOFREAD_ID,
text: this.selectedText,
force_default_locale: true,
},
});
this.diff = suggestion.diff;
this.suggestion = suggestion.suggestions[0];
} catch (e) {
popupAjaxError(e);
} finally {
this.loading = false;
}
}
get selectedText() {
const selected = this.args.model.toolbarEvent.selected;
if (selected.value === "") {
return selected.pre + selected.post;
}
return selected.value;
}
@action
triggerConfirmChanges() {
this.args.closeModal();
if (this.args.model.confirm) {
this.args.model.confirm();
}
if (this.args.model.toolbarEvent && this.suggestion) {
this.args.model.toolbarEvent.replaceText(
this.selectedText,
this.suggestion
);
}
this.args.confirm();
}
@action
triggerRevertChanges() {
this.args.model.revert();
this.args.closeModal();
this.args.revert();
}
<template>
@@ -90,46 +25,30 @@ export default class ModalDiffModal extends Component {
@closeModal={{@closeModal}}
>
<:body>
{{#if this.loading}}
<div class="composer-ai-helper-modal__loading">
<CookText @rawText={{this.selectedText}} />
</div>
{{#if @diff}}
{{htmlSafe @diff}}
{{else}}
{{#if this.diff}}
{{htmlSafe this.diff}}
{{else}}
<div class="composer-ai-helper-modal__old-value">
{{@model.oldValue}}
</div>
<div class="composer-ai-helper-modal__old-value">
{{@oldValue}}
</div>
<div class="composer-ai-helper-modal__new-value">
{{@model.newValue}}
</div>
{{/if}}
<div class="composer-ai-helper-modal__new-value">
{{@newValue}}
</div>
{{/if}}
</:body>
<:footer>
{{#if this.loading}}
<DButton
class="btn-primary"
@label="discourse_ai.ai_helper.context_menu.loading"
@disabled={{true}}
/>
{{else}}
<DButton
class="btn-primary confirm"
@action={{this.triggerConfirmChanges}}
@label="discourse_ai.ai_helper.context_menu.confirm"
/>
{{#if @model.revert}}
<DButton
class="btn-flat revert"
@action={{this.triggerRevertChanges}}
@label="discourse_ai.ai_helper.context_menu.revert"
/>
{{/if}}
{{/if}}
<DButton
class="btn-primary confirm"
@action={{this.triggerConfirmChanges}}
@label="discourse_ai.ai_helper.context_menu.confirm"
/>
<DButton
class="btn-flat"
@action={{this.triggerRevertChanges}}
@label="discourse_ai.ai_helper.context_menu.revert"
/>
</:footer>
</DModal>
</template>
@@ -53,7 +53,7 @@ export default class ThumbnailSuggestions extends Component {
>
<:body>
<div class="ai-thumbnail-suggestions">
{{#each @model.thumbnails as |thumbnail|}}
{{#each @thumbnails as |thumbnail|}}
<ThumbnailSuggestionItem
@thumbnail={{thumbnail}}
@addSelection={{this.addSelection}}
@@ -14,7 +14,6 @@ export default class AiComposerHelper extends Component {
return showComposerAIHelper(outletArgs, helper, "context_menu");
}
@service site;
@service menu;
@service aiComposerHelper;
@tracked caretCoords;
@@ -32,9 +31,6 @@ export default class AiComposerHelper extends Component {
document.addEventListener("mousedown", this.onMouseDown, { passive: true });
document.addEventListener("mouseup", this.onMouseUp, { passive: true });
document.addEventListener("selectionchange", this.onSelectionChanged);
window.visualViewport.addEventListener("resize", this.onResize, {
passive: true,
});
this.dEditorInput = document.querySelector(".d-editor-input");
@@ -46,7 +42,6 @@ export default class AiComposerHelper extends Component {
document.removeEventListener("mousedown", this.onMouseDown);
document.removeEventListener("mouseup", this.onMouseUp);
document.removeEventListener("selectionchange", this.onSelectionChanged);
window.visualViewport.removeEventListener("resize", this.onResize);
if (this.dEditorInput) {
this.dEditorInput.removeEventListener("scroll", this.updatePosition);
@@ -59,18 +54,6 @@ export default class AiComposerHelper extends Component {
this.menuInstance?.close();
}
@bind
onResize() {
if (!this.site.mobileView) {
return;
}
document.documentElement.style.setProperty(
"--mobile-virtual-screen-height",
`${window.visualViewport.height}px`
);
}
@bind
onSelectionChanged() {
if (
@@ -69,10 +69,8 @@ export default class AiSummaryBox extends Component {
}
@bind
subscribe(unsubscribe, [topicId]) {
const sameTopicId = this.args.outletArgs.topic.id === topicId;
if (unsubscribe && this._channel && !sameTopicId) {
subscribe(unsubscribe) {
if (unsubscribe && this._channel) {
this.unsubscribe();
}
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
@@ -177,7 +175,7 @@ export default class AiSummaryBox extends Component {
>
<DMenu
@onShow={{this.generateSummary}}
@arrow={{false}}
@arrow={{true}}
@identifier="topic-map__ai-summary"
@onRegisterApi={{this.onRegisterApi}}
@interactive={{true}}
@@ -1,50 +0,0 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import i18n from "discourse-common/helpers/i18n";
import ModalDiffModal from "../discourse/components/modal/diff-modal";
function initializeProofread(api) {
api.addComposerToolbarPopupMenuOption({
action: (toolbarEvent) => {
const modal = api.container.lookup("service:modal");
const composer = api.container.lookup("service:composer");
const toasts = api.container.lookup("service:toasts");
if (composer.model.reply?.length === 0) {
toasts.error({
duration: 3000,
data: {
message: i18n("discourse_ai.ai_helper.proofread.no_content_error"),
},
});
return;
}
modal.show(ModalDiffModal, {
model: {
toolbarEvent,
},
});
},
icon: "spell-check",
label: "discourse_ai.ai_helper.context_menu.proofread_prompt",
shortcut: "ALT+P",
condition: () => {
const siteSettings = api.container.lookup("service:site-settings");
const currentUser = api.getCurrentUser();
return (
siteSettings.ai_helper_enabled && currentUser?.can_use_assistant_in_post
);
},
});
}
export default {
name: "discourse-ai-helper",
initialize() {
withPluginApi("1.1.0", (api) => {
initializeProofread(api);
});
},
};
@@ -145,9 +145,6 @@ export default apiInitializer("1.25.0", (api) => {
}
const caption = await fetchImageCaption(upload.url, "long_url");
if (!caption) {
return getUploadMarkdown(upload);
}
return `![${caption}|${upload.thumbnail_width}x${upload.thumbnail_height}](${upload.short_url})`;
});
@@ -14,18 +14,6 @@
height: 200px;
}
}
@keyframes fadeOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
&__loading {
animation: fadeOpacity 1.5s infinite alternate;
}
&__old-value {
background-color: var(--danger-low);
@@ -1,87 +0,0 @@
:root {
// Overridden in ai-composer-helper.gjs to adjust for virtual keyboard
--mobile-virtual-screen-height: 100svh;
}
.ai-composer-helper-menu:not(.is-expanded) {
--composer-helper-menu-padding: 1rem;
--composer-helper-menu-trigger-size: 3em;
--composer-helper-menu-height: calc(1rem + 3em);
position: fixed;
list-style: none;
inset: auto;
transform: none;
max-width: unset;
z-index: z("fullscreen");
top: calc(
var(--mobile-virtual-screen-height) - var(--composer-helper-menu-height) -
env(keyboard-inset-height)
);
right: 0;
padding-right: 1rem;
padding-bottom: var(--composer-helper-menu-padding);
margin: 0px;
width: 100vw;
text-align: right;
border: none;
background: linear-gradient(
to bottom,
transparent 0,
rgba(0, 0, 0, 0.3) 100%
);
box-shadow: none;
&.out-of-bounds {
visibility: visible;
pointer-events: initial;
}
button {
background: var(--secondary);
border: 1px solid var(--primary-low);
border-radius: 100%;
box-shadow: 0 0 10px 2px
dark-light-choose(
rgba(var(--primary-rgb), 0.1),
rgba(var(--secondary-rgb), 0.1)
);
width: var(--composer-helper-menu-trigger-size);
height: var(--composer-helper-menu-trigger-size);
animation-duration: 0.15s;
animation-timing-function: cubic-bezier(0, 0.7, 0.9, 1.2);
animation-fill-mode: backwards;
animation-name: slide-in;
&:hover {
.discourse-no-touch & {
background: var(--secondary);
}
}
.d-button-label {
display: none;
}
.d-icon {
margin: 0;
color: var(--tertiary-high);
}
}
}
.ios-safari-composer-hacks .ai-composer-helper-menu:not(.is-expanded) {
top: calc(
var(--mobile-virtual-screen-height) - var(--composer-helper-menu-height)
);
}
@keyframes slide-in {
0% {
opacity: 0;
transform: translateY(calc(100% + 1rem));
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@@ -18,15 +18,6 @@
width: 350px;
}
.ai-llm-editor-provider-param {
&__checkbox {
display: flex;
align-items: flex-start;
flex-direction: row-reverse;
justify-content: left;
}
}
.fk-d-tooltip__icon {
padding-left: 0.25em;
color: var(--primary-medium);
File diff suppressed because it is too large Load Diff
@@ -9,9 +9,14 @@
max-width: 470px !important; //overruling JS
max-height: calc(100vh - var(--header-offset) - 3rem);
&__inner-content {
max-height: unset;
padding: 0;
}
.ai-summary__header,
.ai-summary-box {
padding: 0.75em 1rem;
padding-inline: 1.5rem;
box-sizing: border-box;
}
@@ -31,6 +36,9 @@
margin: 0;
}
}
&-box {
padding-bottom: 1rem;
}
}
}
}
@@ -1,3 +0,0 @@
.ai-summary-box {
padding: 0.75em 1rem;
}
+2 -7
View File
@@ -236,11 +236,11 @@ en:
back: "Back"
confirm_delete: Are you sure you want to delete this model?
delete: Delete
seeded_warning: "This model is pre-configured on your site and cannot be edited."
in_use_warning:
one: "This model is currently used by the %{settings} setting. If misconfigured, the feature won't work as expected."
other: "This model is currently used by the following settings: %{settings}. If misconfigured, features won't work as expected. "
preconfigured_llms: "Select your LLM"
preconfigured:
none: "Configure manually..."
@@ -268,13 +268,11 @@ en:
google: "Google"
azure: "Azure"
ollama: "Ollama"
CDCK: "CDCK"
provider_fields:
access_key_id: "AWS Bedrock Access key ID"
region: "AWS Bedrock Region"
organization: "Optional OpenAI Organization ID"
disable_system_prompt: "Disable system message in prompts"
related_topics:
title: "Related Topics"
@@ -295,13 +293,12 @@ en:
view_changes: "View Changes"
confirm: "Confirm"
revert: "Revert"
changes: "Suggested Edits"
changes: "Changes"
custom_prompt:
title: "Custom Prompt"
placeholder: "Enter a custom prompt..."
submit: "Send Prompt"
translate_prompt: "Translate to %{language}"
proofread_prompt: "Proofread Text"
post_options_menu:
trigger: "Ask AI"
title: "Ask AI"
@@ -330,8 +327,6 @@ en:
prompt: "This post contains non-captioned images. Would you like to enable automatic AI captions on image uploads? (This can be changed in your preferences later)"
confirm: "Enable"
cancel: "Don't ask again"
proofread:
no_content_error: "Unable to proofread text, please add some content to proofread."
reviewables:
model_used: "Model used:"
-4
View File
@@ -11,10 +11,6 @@ de:
title: Beiträge mithilfe von KI sortieren
description: "Beiträge mithilfe eines großen Sprachmodells sortieren"
system_prompt_missing_post_placeholder: "System-Eingabeaufforderung muss einen Platzhalter für den Beitrag enthalten: %%POST%%"
flagged_post: |
<div>Antwort des Modells:</div>
<p>%%LLM_RESPONSE%%</p>
<b>Ausgelöst durch die <a href="/admin/plugins/discourse-automation/%%AUTOMATION_ID%%">%%AUTOMATION_NAME%%</a> Regel.</b>
llm_report:
title: Regelmäßiger Bericht mit KI
description: "Regelmäßiger Bericht auf der Grundlage eines großen Sprachmodells"
+1 -7
View File
@@ -4,10 +4,7 @@ en:
llm_triage:
title: Triage posts using AI
description: "Triage posts using a large language model"
flagged_post: |
<div>Response from the model:</div>
<p>%%LLM_RESPONSE%%</p>
<b>Triggered by the <a href="/admin/plugins/discourse-automation/%%AUTOMATION_ID%%">%%AUTOMATION_NAME%%</a> rule.</b>
system_prompt_missing_post_placeholder: "System prompt must contain a placeholder for the post: %%POST%%"
llm_report:
title: Periodic report using AI
description: "Periodic report based on a large language model"
@@ -79,7 +76,6 @@ en:
ai_summarization_enabled: "Enable the topic summarization module."
ai_summarization_model: "Model to use for summarization."
ai_custom_summarization_allowed_groups: "Groups allowed to use create new summaries."
ai_pm_summarization_allowed_groups: "Groups allowed to create and view summaries in PMs."
ai_bot_enabled: "Enable the AI Bot module."
ai_bot_enable_chat_warning: "Display a warning when PM chat is initiated. Can be overriden by editing the translation string: discourse_ai.ai_bot.pm_warning"
@@ -312,7 +308,6 @@ en:
disable_module_first: "You have to disable %{setting} first."
set_llm_first: "Set %{setting} first."
model_unreachable: "We couldn't get a response from this model. Check your settings first."
invalid_seeded_model: "You can't use this model with this feature."
endpoints:
not_configured: "%{display_name} (not configured)"
configuration_hint:
@@ -322,7 +317,6 @@ en:
delete_failed:
one: "We couldn't delete this model because %{settings} is using it. Update the setting and try again."
other: "We couldn't delete this model because %{settings} are using it. Update the settings and try again."
cannot_edit_builtin: "You can't edit a built-in model."
embeddings:
configuration:
-4
View File
@@ -11,10 +11,6 @@ he:
title: מיון ראשוני של פוסטים באמצעות בינה מלאכותית
description: "מיון ראשוני של פוסטים באמצעות דגם שפה גדול"
system_prompt_missing_post_placeholder: "בקשת המערכת חייבת להכיל ממלא מקום לפוסט: %%POST%%"
flagged_post: |
<div>תגובה מהמודל:</div>
<p>%%LLM_RESPONSE%%</p>
<b>הוזנקה על ידי הכלל <a href="/admin/plugins/discourse-automation/%%AUTOMATION_ID%%">%%AUTOMATION_NAME%%</a>.</b>
llm_report:
title: דוח תקופתי בעזרת בינה מלאכותית
description: "דוח תקופתי שמבוסס על דגם שפה גדול"
-29
View File
@@ -289,16 +289,6 @@ discourse_ai:
default: "10" # 10: @trust_level_0
allow_any: false
refresh: true
ai_helper_model_allowed_seeded_models:
default: ""
hidden: true
type: list
list_type: compact
ai_helper_image_caption_model_allowed_seeded_models:
default: ""
hidden: true
type: list
list_type: compact
ai_embeddings_enabled:
default: false
@@ -350,11 +340,6 @@ discourse_ai:
allow_any: false
enum: "DiscourseAi::Configuration::LlmEnumerator"
validator: "DiscourseAi::Configuration::LlmValidator"
ai_embeddings_semantic_search_hyde_model_allowed_seeded_models:
default: ""
hidden: true
type: list
list_type: compact
ai_embeddings_semantic_quick_search_enabled:
default: false
client: true
@@ -368,10 +353,6 @@ discourse_ai:
type: enum
enum: "DiscourseAi::Configuration::LlmEnumerator"
validator: "DiscourseAi::Configuration::LlmValidator"
ai_pm_summarization_allowed_groups:
type: group_list
list_type: compact
default: ""
ai_custom_summarization_allowed_groups:
type: group_list
list_type: compact
@@ -381,11 +362,6 @@ discourse_ai:
default: ""
hidden: true
choices: "DiscourseAi::Configuration::LlmEnumerator.old_summarization_options + ['']"
ai_summarization_model_allowed_seeded_models:
default: ""
hidden: true
type: list
list_type: compact
ai_bot_enabled:
default: false
@@ -426,8 +402,3 @@ discourse_ai:
ai_automation_max_triage_per_post_per_minute:
default: 2
hidden: true
ai_automation_allowed_seeded_models:
default: ""
hidden: true
type: list
list_type: compact
+11 -2
View File
@@ -9,7 +9,17 @@ if defined?(DiscourseAutomation)
triggerables %i[post_created_edited]
field :system_prompt, component: :message, required: false
field :system_prompt,
component: :message,
required: true,
validator: ->(input) do
if !input.include?("%%POST%%")
I18n.t(
"discourse_automation.scriptables.llm_triage.system_prompt_missing_post_placeholder",
)
end
end,
accepts_placeholders: true
field :search_for_text, component: :text, required: true
field :model,
component: :choices,
@@ -68,7 +78,6 @@ if defined?(DiscourseAutomation)
canned_reply_user: canned_reply_user,
hide_topic: hide_topic,
flag_post: flag_post,
automation: self.automation,
)
rescue => e
Discourse.warn_exception(e, message: "llm_triage: skipped triage on post #{post.id}")
+2 -17
View File
@@ -31,35 +31,20 @@ module DiscourseAi
parameters[:query].to_s
end
def all_settings
@all_settings ||= SiteSetting.all_settings
end
def all_settings=(settings)
# this is only used for testing
@all_settings = settings
end
def invoke
@last_num_results = 0
terms = query.split(",").map(&:strip).map(&:downcase).reject(&:blank?)
terms_regexes =
terms.map do |term|
regex_string = term.split(/[ _\.\|]/).map { |t| Regexp.escape(t) }.join(".*")
Regexp.new(regex_string, Regexp::IGNORECASE)
end
found =
all_settings.filter do |setting|
SiteSetting.all_settings.filter do |setting|
name = setting[:setting].to_s.downcase
description = setting[:description].to_s.downcase
plugin = setting[:plugin].to_s.downcase
search_string = "#{name} #{description} #{plugin}"
terms_regexes.any? { |regex| search_string.match?(regex) }
terms.any? { |term| search_string.include?(term) }
end
if found.blank?

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