Compare commits

..
Author SHA1 Message Date
Sam Saffron a8f54e2709 DEV: adding a concept of prompt builder
This is half baked and not working, but starts demonstrating the idea
2023-08-25 15:54:16 +10:00
238 changed files with 2594 additions and 39475 deletions
-1
View File
@@ -1,4 +1,3 @@
< 3.2.0.beta2-dev: 3bced1c6f525553fe2b5b79d29bd66c1cf2a47d5
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
< 3.2.0.beta1-dev: 3e7c99de894b03e9161d9481da118fe893991a1d
3.1.0.beta3: 057fbe1ce6
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "eslint-config-discourse"
}
-1
View File
@@ -1 +0,0 @@
module.exports = require("@discourse/lint-configs/eslint");
+1
View File
@@ -0,0 +1 @@
{}
-1
View File
@@ -1 +0,0 @@
module.exports = require("@discourse/lint-configs/prettier");
-2
View File
@@ -1,4 +1,2 @@
inherit_gem:
rubocop-discourse: stree-compat.yml
RSpec/NamedSubject:
Enabled: false
+1 -1
View File
@@ -1,2 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma,plugin/disable_auto_ternary
--plugins=plugin/trailing_comma,disable_ternary
+4 -1
View File
@@ -1 +1,4 @@
module.exports = require("@discourse/lint-configs/template-lint");
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};
+1
View File
@@ -5,4 +5,5 @@ source "https://rubygems.org"
group :development do
gem "rubocop-discourse"
gem "syntax_tree"
gem "syntax_tree-disable_ternary"
end
+20 -28
View File
@@ -2,45 +2,36 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.6.3)
language_server-protocol (3.17.0.3)
parallel (1.23.0)
parser (3.2.2.4)
json (2.6.2)
parallel (1.22.1)
parser (3.1.2.1)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
racc (1.7.2)
prettier_print (1.2.0)
rainbow (3.1.1)
regexp_parser (2.8.2)
rexml (3.2.6)
rubocop (1.57.2)
regexp_parser (2.6.0)
rexml (3.2.5)
rubocop (1.36.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0)
rubocop-ast (>= 1.20.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.19.0)
rubocop (~> 1.41)
rubocop-discourse (3.4.0)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.21.0)
parser (>= 3.1.1.0)
rubocop-discourse (3.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-factory_bot (2.24.0)
rubocop-rspec (2.13.2)
rubocop (~> 1.33)
rubocop-rspec (2.25.0)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
syntax_tree (6.2.0)
ruby-progressbar (1.11.0)
syntax_tree (5.1.0)
prettier_print (>= 1.2.0)
unicode-display_width (2.5.0)
syntax_tree-disable_ternary (1.0.0)
unicode-display_width (2.3.0)
PLATFORMS
arm64-darwin-20
@@ -53,6 +44,7 @@ PLATFORMS
DEPENDENCIES
rubocop-discourse
syntax_tree
syntax_tree-disable_ternary
BUNDLED WITH
2.4.21
2.3.10
+2 -2
View File
@@ -1,5 +1,5 @@
# **Discourse AI** Plugin
# **Plugin Name** Plugin
**Plugin Summary**
For more information, please see: https://meta.discourse.org/t/discourse-ai/259214?u=falco
For more information, please see: **url to meta topic**
@@ -6,7 +6,6 @@ module DiscourseAi
requires_plugin ::DiscourseAi::PLUGIN_NAME
requires_login
before_action :ensure_can_request_suggestions
before_action :rate_limiter_performed!, except: %i[prompts]
def prompts
render json:
@@ -18,100 +17,22 @@ module DiscourseAi
end
def suggest
input = get_text_param!
raise Discourse::InvalidParameters.new(:text) if params[:text].blank?
prompt = CompletionPrompt.find_by(id: params[:mode])
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
if prompt.prompt_type == "custom_prompt" && params[:custom_prompt].blank?
raise Discourse::InvalidParameters.new(:custom_prompt)
end
hijack do
render json:
DiscourseAi::AiHelper::LlmPrompt.new.generate_and_send_prompt(prompt, params),
status: 200
end
rescue ::DiscourseAi::Inference::OpenAiCompletions::CompletionFailed,
::DiscourseAi::Inference::HuggingFaceTextGeneration::CompletionFailed,
::DiscourseAi::Inference::AnthropicCompletions::CompletionFailed => e
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
status: 502
end
def suggest_title
input = get_text_param!
input_hash = { text: input }
llm_prompt =
DiscourseAi::AiHelper::LlmPrompt
.new
.available_prompts(name_filter: "generate_titles")
.first
prompt = CompletionPrompt.find_by(id: llm_prompt[:id])
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
hijack do
render json:
DiscourseAi::AiHelper::LlmPrompt.new.generate_and_send_prompt(
prompt,
input_hash,
params[:text],
),
status: 200
end
rescue ::DiscourseAi::Inference::OpenAiCompletions::CompletionFailed,
::DiscourseAi::Inference::HuggingFaceTextGeneration::CompletionFailed,
::DiscourseAi::Inference::AnthropicCompletions::CompletionFailed => e
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
status: 502
end
def suggest_category
input = get_text_param!
input_hash = { text: input }
render json:
DiscourseAi::AiHelper::SemanticCategorizer.new(
input_hash,
current_user,
).categories,
status: 200
end
def suggest_tags
input = get_text_param!
input_hash = { text: input }
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(input_hash, current_user).tags,
status: 200
end
def suggest_thumbnails
input = get_text_param!
hijack do
thumbnails = DiscourseAi::AiHelper::Painter.new.commission_thumbnails(input, current_user)
render json: { thumbnails: thumbnails }, status: 200
end
end
def explain
post_id = get_post_param!
text = get_text_param!
post = Post.find_by(id: post_id)
raise Discourse::InvalidParameters.new(:post_id) unless post
render json:
DiscourseAi::AiHelper::TopicHelper.new(
{ text: text },
current_user,
post: post,
).explain,
status: 200
rescue ::DiscourseAi::Inference::OpenAiCompletions::CompletionFailed,
::DiscourseAi::Inference::HuggingFaceTextGeneration::CompletionFailed,
::DiscourseAi::Inference::AnthropicCompletions::CompletionFailed => e
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
status: 502
@@ -119,18 +40,6 @@ module DiscourseAi
private
def get_text_param!
params[:text].tap { |t| raise Discourse::InvalidParameters.new(:text) if t.blank? }
end
def get_post_param!
params[:post_id].tap { |t| raise Discourse::InvalidParameters.new(:post_id) if t.blank? }
end
def rate_limiter_performed!
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
end
def ensure_can_request_suggestions
user_group_ids = current_user.group_ids
@@ -8,11 +8,8 @@ module DiscourseAi
SEMANTIC_SEARCH_TYPE = "semantic_search"
def search
query = params[:q].to_s
if query.length < SiteSetting.min_search_term_length
raise Discourse::InvalidParameters.new(:q)
end
query = params[:q]
page = (params[:page] || 1).to_i
grouped_results =
Search::GroupedSearchResults.new(
@@ -22,19 +19,12 @@ module DiscourseAi
use_pg_headlines_for_excerpt: false,
)
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
DiscourseAi::Embeddings::SemanticSearch
.new(guardian)
.search_for_topics(query, page)
.each { |topic_post| grouped_results.add(topic_post) }
if !semantic_search.cached_query?(query)
RateLimiter.new(current_user, "semantic-search", 4, 1.minutes).performed!
end
hijack do
semantic_search
.search_for_topics(query)
.each { |topic_post| grouped_results.add(topic_post) }
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
end
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
end
end
end
-2
View File
@@ -15,8 +15,6 @@ end
# id :bigint not null, primary key
# provider_id :integer not null
# user_id :integer
# topic_id :integer
# post_id :integer
# request_tokens :integer
# response_tokens :integer
# raw_request_payload :string
+4 -20
View File
@@ -10,26 +10,10 @@ class CompletionPrompt < ActiveRecord::Base
validate :each_message_length
def messages_with_user_input(user_input)
return messages unless user_input.present?
if user_input[:custom_prompt].present?
case ::DiscourseAi::AiHelper::LlmPrompt.new.enabled_provider
when "huggingface"
self.messages.each { |msg| msg.sub!("{{custom_prompt}}", user_input[:custom_prompt]) }
else
self.messages.each do |msg|
msg["content"].sub!("{{custom_prompt}}", user_input[:custom_prompt])
end
end
end
case ::DiscourseAi::AiHelper::LlmPrompt.new.enabled_provider
when "openai"
self.messages << { role: "user", content: user_input[:text] }
when "anthropic"
self.messages << { "role" => "Input", "content" => "<input>#{user_input[:text]}</input>" }
when "huggingface"
self.messages.first.sub("{{user_input}}", user_input[:text])
if ::DiscourseAi::AiHelper::LlmPrompt.new.enabled_provider == "openai"
self.messages << { role: "user", content: user_input }
else
self.messages << { "role" => "Input", "content" => "<input>#{user_input}</input>" }
end
end
@@ -1,9 +1,10 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "I18n";
import Component from "@glimmer/component";
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
import I18n from "I18n";
export default class AiBotHeaderPanel extends Component {
@service siteSettings;
@service composer;
@@ -0,0 +1,69 @@
<DModalBody @title="discourse_ai.ai_helper.title">
<span>{{i18n "discourse_ai.ai_helper.description"}}</span>
<ComboBox
@value={{this.selected}}
@content={{this.helperOptions}}
@onChange={{action this.updateSelected}}
@valueProperty="value"
@class="ai-helper-mode"
/>
<div class="text-preview">
<Textarea
@value={{this.composedMessage}}
disabled="true"
class="preview-area"
/>
</div>
<div class="selection-hint">{{i18n
"discourse_ai.ai_helper.selection_hint"
}}</div>
<div class="text-preview">
<ConditionalLoadingSpinner @condition={{this.loading}} />
{{#unless this.loading}}
{{#if this.selectingTopicTitle}}
<div class="radios">
{{#each this.generatedTitlesSuggestions as |title index|}}
<label class="radio-label" for="title-suggestion-{{index}}">
<RadioButton
@id="title-suggestion-{{index}}"
@name="title-suggestion"
@value={{title}}
@selection={{this.selectedTitle}}
/>
<b>{{title}}</b>
</label>
{{/each}}
</div>
{{else if this.proofreadingText}}
{{html-safe this.proofreadDiff}}
{{else if this.translatingText}}
<Textarea
@value={{this.translatedSuggestion}}
disabled="true"
class="preview-area"
/>
{{/if}}
{{/unless}}
</div>
</DModalBody>
<div class="modal-footer">
{{#if this.canSave}}
<DButton
@class="btn-primary create"
@action={{this.applySuggestion}}
@label="save"
/>
<DModalCancel @close={{route-action "closeModal"}} />
{{else}}
<div class="ai-helper-waiting-selection">{{i18n
"discourse_ai.modals.select_option"
}}</div>
{{/if}}
</div>
@@ -0,0 +1,152 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action, computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
const LIST = "list";
const TEXT = "text";
const DIFF = "diff";
export default class AiHelper extends Component {
@tracked selected = null;
@tracked loading = false;
@tracked generatedTitlesSuggestions = [];
@tracked proofReadSuggestion = null;
@tracked translatedSuggestion = null;
@tracked selectedTitle = null;
@tracked proofreadDiff = null;
@tracked helperOptions = [];
prompts = [];
promptTypes = {};
constructor() {
super(...arguments);
this.loadPrompts();
}
async loadPrompts() {
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
prompts.map((p) => {
this.prompts[p.id] = p;
});
this.promptTypes = prompts.reduce((memo, p) => {
memo[p.name] = p.prompt_type;
return memo;
}, {});
this.helperOptions = prompts.map((p) => {
return {
name: p.translated_name,
value: p.id,
};
});
}
get composedMessage() {
const editor = this.args.editor;
return editor.getSelected().value || editor.value;
}
@computed("selected", "selectedTitle", "translatingText", "proofreadingText")
get canSave() {
return (
(this.selected &&
this.prompts[this.selected].prompt_type === LIST &&
this.selectedTitle) ||
this.translatingText ||
this.proofreadingText
);
}
@computed("selected", "translatedSuggestion")
get translatingText() {
return (
this.selected &&
this.prompts[this.selected].prompt_type === TEXT &&
this.translatedSuggestion
);
}
@computed("selected", "proofReadSuggestion")
get proofreadingText() {
return (
this.selected &&
this.prompts[this.selected].prompt_type === DIFF &&
this.proofReadSuggestion
);
}
@computed("selected", "generatedTitlesSuggestions")
get selectingTopicTitle() {
return (
this.selected &&
this.prompts[this.selected].prompt_type === LIST &&
this.generatedTitlesSuggestions.length > 0
);
}
_updateSuggestedByAI(data) {
switch (data.type) {
case LIST:
this.generatedTitlesSuggestions = data.suggestions;
break;
case TEXT:
this.translatedSuggestion = data.suggestions[0];
break;
case DIFF:
this.proofReadSuggestion = data.suggestions[0];
this.proofreadDiff = data.diff;
break;
}
}
@action
async updateSelected(value) {
this.loading = true;
this.selected = value;
if (value === LIST) {
this.selectedTitle = null;
}
if (this.hasSuggestion) {
this.loading = false;
} else {
return ajax("/discourse-ai/ai-helper/suggest", {
method: "POST",
data: { mode: this.selected, text: this.composedMessage },
})
.then((data) => {
this._updateSuggestedByAI(data);
})
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
}
@action
applySuggestion() {
if (this.selectingTopicTitle) {
const composer = this.args.editor.outletArgs?.composer;
if (composer) {
composer.set("title", this.selectedTitle);
}
} else {
const newText = this.proofreadingText
? this.proofReadSuggestion
: this.translatedSuggestion;
this.args.editor.replaceText(this.composedMessage, newText);
}
this.args.closeModal();
}
}
@@ -1,239 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse-common/utils/decorators";
import I18n from "I18n";
export default class AISuggestionDropdown extends Component {
@service dialog;
@service siteSettings;
@service composer;
@tracked loading = false;
@tracked showMenu = false;
@tracked generatedSuggestions = [];
@tracked suggestIcon = "discourse-sparkles";
@tracked showErrors = false;
@tracked error = "";
SUGGESTION_TYPES = {
title: "suggest_title",
category: "suggest_category",
tag: "suggest_tags",
};
willDestroy() {
super.willDestroy(...arguments);
document.removeEventListener("click", this.onClickOutside);
}
get showAIButton() {
const minCharacterCount = 40;
const isShowAIButton = this.composer.model.replyLength > minCharacterCount;
const composerFields = document.querySelector(".composer-fields");
if (composerFields) {
if (isShowAIButton) {
composerFields.classList.add("showing-ai-suggestions");
} else {
composerFields.classList.remove("showing-ai-suggestions");
}
}
return isShowAIButton;
}
get disableSuggestionButton() {
return this.loading;
}
@action
applyClasses() {
if (this.showAIButton) {
document.querySelector(".composer-fields")?.classList.add("showing-ai-suggestions");
} else {
document.querySelector(".composer-fields")?.classList.remove("showing-ai-suggestions");
}
}
@bind
onClickOutside(event) {
const menu = document.querySelector(".ai-title-suggestions-menu");
if (event.target === menu) {
return;
}
return this.#closeMenu();
}
@action
handleClickOutside() {
document.addEventListener("click", this.onClickOutside);
}
@action
applySuggestion(suggestion) {
if (!this.args.mode) {
return;
}
const composer = this.args?.composer;
if (!composer) {
return;
}
if (this.args.mode === this.SUGGESTION_TYPES.title) {
composer.set("title", suggestion);
return this.#closeMenu();
}
if (this.args.mode === this.SUGGESTION_TYPES.category) {
const selectedCategoryId = this.composer.categories.find(
(c) => c.slug === suggestion
).id;
composer.set("categoryId", selectedCategoryId);
return this.#closeMenu();
}
if (this.args.mode === this.SUGGESTION_TYPES.tag) {
this.#updateTags(suggestion, composer);
}
}
@action
async performSuggestion() {
if (!this.args.mode) {
return;
}
if (this.composer.model.replyLength === 0) {
return this.dialog.alert(
I18n.t("discourse_ai.ai_helper.missing_content")
);
}
this.loading = true;
this.suggestIcon = "spinner";
return ajax(`/discourse-ai/ai-helper/${this.args.mode}`, {
method: "POST",
data: { text: this.composer.model.reply },
})
.then((data) => {
this.#assignGeneratedSuggestions(data, this.args.mode);
})
.catch(popupAjaxError)
.finally(() => {
this.loading = false;
this.suggestIcon = "sync-alt";
this.showMenu = true;
if (this.args.mode === "suggest_category") {
document.querySelector(".category-input")?.classList.add("showing-ai-suggestion-menu");
}
});
}
#closeMenu() {
if (this.showMenu && this.args.mode === "suggest_category") {
document.querySelector(".category-input")?.classList.remove("showing-ai-suggestion-menu");
}
this.suggestIcon = "discourse-sparkles";
this.showMenu = false;
this.showErrors = false;
this.errors = "";
}
#updateTags(suggestion, composer) {
const maxTags = this.siteSettings.max_tags_per_topic;
if (!composer.tags) {
composer.set("tags", [suggestion]);
// remove tag from the list of suggestions once added
this.generatedSuggestions = this.generatedSuggestions.filter(
(s) => s !== suggestion
);
return;
}
const tags = composer.tags;
if (tags?.length >= maxTags) {
// Show error if trying to add more tags than allowed
this.showErrors = true;
this.error = I18n.t("select_kit.max_content_reached", { count: maxTags });
return;
}
tags.push(suggestion);
composer.set("tags", [...tags]);
// remove tag from the list of suggestions once added
return (this.generatedSuggestions = this.generatedSuggestions.filter(
(s) => s !== suggestion
));
}
#tagSelectorHasValues() {
return this.args.composer?.tags && this.args.composer?.tags.length > 0;
}
#assignGeneratedSuggestions(data, mode) {
if (mode === this.SUGGESTION_TYPES.title) {
return (this.generatedSuggestions = data.suggestions);
}
const suggestions = data.assistant.map((s) => s.name);
if (mode === this.SUGGESTION_TYPES.tag) {
if (this.#tagSelectorHasValues()) {
// Filter out tags if they are already selected in the tag input
return (this.generatedSuggestions = suggestions.filter(
(t) => !this.args.composer.tags.includes(t)
));
} else {
return (this.generatedSuggestions = suggestions);
}
}
return (this.generatedSuggestions = suggestions);
}
<template>
{{#if this.showAIButton}}
<DButton
@class="suggestion-button {{if this.loading 'is-loading'}}"
@icon={{this.suggestIcon}}
@title="discourse_ai.ai_helper.suggest"
@action={{this.performSuggestion}}
@disabled={{this.disableSuggestionButton}}
...attributes
/>
{{/if}}
{{#if this.showMenu}}
{{! template-lint-disable modifier-name-case }}
<ul
class="popup-menu ai-suggestions-menu"
{{didInsert this.handleClickOutside}}
>
{{#if this.showErrors}}
<li class="ai-suggestions-menu__errors">{{this.error}}</li>
{{/if}}
{{#each this.generatedSuggestions as |suggestion index|}}
<li data-name={{suggestion}} data-value={{index}}>
<DButton
@class="popup-menu-btn"
@translatedLabel={{suggestion}}
@action={{this.applySuggestion}}
@actionParam={{suggestion}}
/>
</li>
{{/each}}
</ul>
{{/if}}
</template>
}
@@ -1,25 +1,14 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import DModalCancel from "discourse/components/d-modal-cancel";
import I18n from "I18n";
import { htmlSafe } from "@ember/template";
const t = I18n.t.bind(I18n);
export default class ModalDiffModal extends Component {
@action
triggerConfirmChanges() {
this.args.closeModal();
this.args.confirm();
}
@action
triggerRevertChanges() {
this.args.closeModal();
this.args.revert();
}
<template>
<DModal
class="composer-ai-helper-modal"
@@ -46,12 +35,14 @@ export default class ModalDiffModal extends Component {
@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"
/>
<DModalCancel @close={{@closeModal}} />
</:footer>
</DModal>
</template>
@action
triggerConfirmChanges() {
this.args.closeModal();
this.args.confirm();
}
}
@@ -1,22 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import { showComposerAIHelper } from "../../lib/show-ai-helper";
export default class AICategorySuggestion extends Component {
static shouldRender(outletArgs, helper) {
return showComposerAIHelper(outletArgs, helper);
}
@service siteSettings;
<template>
{{#if this.siteSettings.ai_embeddings_enabled}}
<AISuggestionDropdown
@mode="suggest_category"
@composer={{@outletArgs.composer}}
class="suggest-category-button"
/>
{{/if}}
</template>
}
@@ -1,22 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import { showComposerAIHelper } from "../../lib/show-ai-helper";
export default class AITagSuggestion extends Component {
static shouldRender(outletArgs, helper) {
return showComposerAIHelper(outletArgs, helper);
}
@service siteSettings;
<template>
{{#if this.siteSettings.ai_embeddings_enabled}}
<AISuggestionDropdown
@mode="suggest_tags"
@composer={{@outletArgs.composer}}
class="suggest-tags-button"
/>
{{/if}}
</template>
}
@@ -1,17 +0,0 @@
import Component from "@glimmer/component";
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import { showComposerAIHelper } from "../../lib/show-ai-helper";
export default class AITitleSuggestion extends Component {
static shouldRender(outletArgs, helper) {
return showComposerAIHelper(outletArgs, helper);
}
<template>
<AISuggestionDropdown
@mode="suggest_title"
@composer={{@outletArgs.composer}}
class="suggest-titles-button"
/>
</template>
}
@@ -5,7 +5,7 @@
<ul class="ai-helper-context-menu__trigger">
<li>
<DButton
@icon="discourse-sparkles"
@icon="magic"
@action={{this.toggleAiHelperOptions}}
@label="discourse_ai.ai_helper.context_menu.trigger"
class="btn-flat"
@@ -16,55 +16,40 @@
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.options)}}
<ul class="ai-helper-context-menu__options">
{{#each this.helperOptions as |option|}}
{{#if (eq option.name "custom_prompt")}}
<div
class="ai-custom-prompt"
{{did-insert this.setupCustomPrompt}}
>
<Input
@value={{this.customPromptValue}}
placeholder={{i18n
"discourse_ai.ai_helper.context_menu.custom_prompt.placeholder"
}}
class="ai-custom-prompt__input"
@enter={{action (fn this.updateSelected option)}}
/>
<li data-name={{option.name}} data-value={{option.value}}>
<DButton
@class="btn-flat"
@translatedLabel={{option.name}}
@action={{this.updateSelected}}
@actionParam={{option.value}}
/>
</li>
{{/each}}
</ul>
<DButton
@class="ai-custom-prompt__submit btn-primary"
@icon="discourse-sparkles"
@action={{this.updateSelected}}
@actionParam={{option}}
@disabled={{not this.customPromptValue.length}}
/>
</div>
{{else}}
<li data-name={{option.translated_name}} data-value={{option.id}}>
<DButton
@icon={{option.icon}}
@class="btn-flat"
@translatedLabel={{option.translated_name}}
@action={{this.updateSelected}}
@actionParam={{option}}
/>
</li>
{{/if}}
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.suggestions)}}
<ul class="ai-helper-context-menu__suggestions">
{{#each this.generatedTitleSuggestions as |suggestion index|}}
<li data-name={{suggestion}} data-value={{index}}>
<DButton
@class="btn-flat"
@translatedLabel={{suggestion}}
@action={{this.updateTopicTitle}}
@actionParam={{suggestion}}
/>
</li>
{{/each}}
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.loading)}}
<div class="ai-helper-context-menu__loading">
<div class="dot-falling"></div>
<span>
{{i18n "discourse_ai.ai_helper.context_menu.loading"}}
</span>
<DButton
@icon="times"
@title="discourse_ai.ai_helper.context_menu.cancel"
@action={{this.cancelAIAction}}
class="btn-flat cancel-request"
/>
</div>
<ul class="ai-helper-context-menu__loading">
<li>
<div class="dot-falling"></div>
<span>
{{i18n "discourse_ai.ai_helper.context_menu.loading"}}
</span>
</li>
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.review)}}
<ul class="ai-helper-context-menu__review">
@@ -114,6 +99,7 @@
/>
</li>
</ul>
{{/if}}
</div>
{{/if}}
@@ -125,7 +111,6 @@
@diff={{this.diff}}
@oldValue={{this.selectedText}}
@newValue={{this.newSelectedText}}
@revert={{this.undoAIAction}}
@closeModal={{fn (mut this.showDiffModal) false}}
/>
{{/if}}
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
{{#if this.isAiBotChat}}
<DSection @bodyClass={{this.aiBotClasses}} />
<DSection @bodyClass="ai-bot-chat" />
{{#if this.renderChatWarning}}
<div class="ai-bot-chat-warning">{{i18n
"discourse_ai.ai_bot.pm_warning"
@@ -1,9 +1,12 @@
import Component from "@glimmer/component";
import { computed } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "discourse-i18n";
import { computed } from "@ember/object";
export default class extends Component {
static shouldRender() {
return true;
}
@service currentUser;
@service siteSettings;
@@ -15,18 +18,6 @@ export default class extends Component {
return this.siteSettings.ai_bot_enable_chat_warning;
}
@computed("composerModel.targetRecipients", "composerModel.title")
get aiBotClasses() {
if (
this.composerModel?.title ===
I18n.t("discourse_ai.ai_bot.default_pm_prefix")
) {
return "ai-bot-chat";
} else {
return "ai-bot-pm";
}
}
@computed("composerModel.targetRecipients")
get isAiBotChat() {
if (
@@ -1,7 +0,0 @@
<div class="gpt-persona">
<DropdownSelectBox
@value={{this.value}}
@content={{this.botOptions}}
@options={{hash icon="robot"}}
/>
</div>
@@ -1,53 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
function isBotMessage(composer, currentUser) {
if (
composer &&
composer.targetRecipients &&
currentUser.ai_enabled_chat_bots
) {
const reciepients = composer.targetRecipients.split(",");
return currentUser.ai_enabled_chat_bots.any((bot) =>
reciepients.any((username) => username === bot.username)
);
}
return false;
}
export default class BotSelector extends Component {
static shouldRender(args, container) {
return (
container?.currentUser?.ai_enabled_personas &&
isBotMessage(args.model, container.currentUser)
);
}
@service currentUser;
get composer() {
return this.args?.outletArgs?.model;
}
get botOptions() {
if (this.currentUser.ai_enabled_personas) {
return this.currentUser.ai_enabled_personas.map((persona) => {
return {
id: persona.name,
name: persona.name,
description: persona.description,
};
});
}
}
get value() {
return this._value || this.botOptions[0].id;
}
set value(val) {
this._value = val;
this.composer.metaData = { ai_persona: val };
}
}
@@ -1,51 +0,0 @@
{{#if this.searchEnabled}}
<div class="semantic-search__container search-results" role="region">
<div
class="semantic-search__results"
{{did-insert this.setup}}
{{did-insert this.debouncedSearch}}
{{will-destroy this.teardown}}
>
{{#if this.searching}}
<div class="semantic-search__searching">
<div class="semantic-search__searching-text">
{{i18n "discourse_ai.embeddings.semantic_search_loading"}}
</div>
<span class="semantic-search__indicator-wave">
<span class="semantic-search__indicator-dot">.</span>
<span class="semantic-search__indicator-dot">.</span>
<span class="semantic-search__indicator-dot">.</span>
</span>
</div>
{{else}}
{{#if this.results.length}}
<div class="semantic-search__toggle-button-container">
<DButton
@translatedTitle={{this.collapsedResultsTitle}}
@translatedLabel={{this.collapsedResultsTitle}}
@action={{fn
(mut this.collapsedResults)
(not this.collapsedResults)
}}
@class="btn-flat"
@icon={{if this.collapsedResults "chevron-right" "chevron-down"}}
/>
</div>
{{#unless this.collapsedResults}}
<div class="semantic-search__entries">
<SearchResultEntries
@posts={{this.results}}
@highlightQuery={{this.highlightQuery}}
/>
</div>
{{/unless}}
{{else}}
<div class="semantic-search__searching">
{{i18n "discourse_ai.embeddings.semantic_search_results.none"}}
</div>
{{/if}}
{{/if}}
</div>
</div>
{{/if}}

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