Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d544f2101b | ||
|
|
2d299900b9 | ||
|
|
c7781c57fc | ||
|
|
db11fe391d |
@@ -1,3 +1 @@
|
||||
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
|
||||
< 3.2.0.beta1-dev: 3e7c99de894b03e9161d9481da118fe893991a1d
|
||||
3.1.0.beta3: 057fbe1ce6
|
||||
|
||||
@@ -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,94 +17,29 @@ 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
|
||||
|
||||
private
|
||||
|
||||
def get_text_param!
|
||||
params[:text].tap { |t| raise Discourse::InvalidParameters.new(:text) 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,17 @@ module DiscourseAi
|
||||
use_pg_headlines_for_excerpt: false,
|
||||
)
|
||||
|
||||
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
||||
model =
|
||||
DiscourseAi::Embeddings::Model.instantiate(
|
||||
SiteSetting.ai_embeddings_semantic_search_model,
|
||||
)
|
||||
|
||||
if !semantic_search.cached_query?(query)
|
||||
RateLimiter.new(current_user, "semantic-search", 4, 1.minutes).performed!
|
||||
end
|
||||
DiscourseAi::Embeddings::SemanticSearch
|
||||
.new(guardian, model)
|
||||
.search_for_topics(query, page)
|
||||
.each { |topic_post| grouped_results.add(topic_post) }
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module Summarization
|
||||
class SummaryController < ::ApplicationController
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
requires_login
|
||||
|
||||
VALID_SINCE_VALUES = [1, 3, 6, 12, 24, 72, 168]
|
||||
VALID_TARGETS = %w[chat_channel topic]
|
||||
|
||||
def show
|
||||
raise PluginDisabled unless SiteSetting.ai_summarization_enabled
|
||||
target_type = params[:target_type]
|
||||
|
||||
raise Discourse::InvalidParameters.new(:target_type) if !VALID_TARGETS.include?(target_type)
|
||||
|
||||
since = nil
|
||||
|
||||
if target_type == "chat_channel"
|
||||
since = params[:since].to_i
|
||||
raise Discourse::InvalidParameters.new(:since) if !VALID_SINCE_VALUES.include?(since)
|
||||
target = Chat::Channel.find_by(id: params[:target_id])
|
||||
raise Discourse::NotFound.new(:chat_channel) if !target
|
||||
raise Discourse::InvalidAccess if !guardian.can_join_chat_channel?(target)
|
||||
else
|
||||
target = Topic.find_by(id: params[:target_id])
|
||||
raise Discourse::NotFound.new(:topic) if !target
|
||||
raise Discourse::InvalidAccess if !guardian.can_see_topic?(target)
|
||||
end
|
||||
|
||||
RateLimiter.new(
|
||||
current_user,
|
||||
"ai_summarization",
|
||||
6,
|
||||
SiteSetting.ai_summarization_rate_limit_minutes.minutes,
|
||||
).performed!
|
||||
|
||||
hijack do
|
||||
summary =
|
||||
DiscourseAi::Summarization::SummaryGenerator.new(target, current_user).summarize!(since)
|
||||
|
||||
render json: { summary: summary }, status: 200
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,6 @@ class AiApiAuditLog < ActiveRecord::Base
|
||||
module Provider
|
||||
OpenAI = 1
|
||||
Anthropic = 2
|
||||
HuggingFaceTextGeneration = 3
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,24 +10,10 @@ class CompletionPrompt < ActiveRecord::Base
|
||||
validate :each_message_length
|
||||
|
||||
def messages_with_user_input(user_input)
|
||||
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,11 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AiChatChannelSerializer < ApplicationSerializer
|
||||
attributes :id, :chatable, :chatable_type, :chatable_url, :slug
|
||||
|
||||
def title
|
||||
# Display all participants for a DM.
|
||||
# For category channels, the argument is ignored.
|
||||
object.title(nil)
|
||||
end
|
||||
attributes :id, :chatable, :chatable_type, :chatable_url, :title, :slug
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class AiBotHeaderPanel extends Component {
|
||||
@service composer;
|
||||
|
||||
@action
|
||||
composeMessageWithTargetBot(target) {
|
||||
async composeMessageWithTargetBot(target) {
|
||||
this.#composeAiBotMessage(target);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class AiBotHeaderPanel extends Component {
|
||||
return this.siteSettings.ai_bot_enabled_chat_bots.split("|");
|
||||
}
|
||||
|
||||
#composeAiBotMessage(targetBot) {
|
||||
async #composeAiBotMessage(targetBot) {
|
||||
this.args.closePanel();
|
||||
composeAiBotMessage(targetBot, this.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,196 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { inject as service } from "@ember/service";
|
||||
import I18n from "I18n";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export default class AISuggestionDropdown extends Component {
|
||||
<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>
|
||||
|
||||
@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;
|
||||
return this.composer.model.replyLength > minCharacterCount;
|
||||
}
|
||||
|
||||
get disableSuggestionButton() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
@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;
|
||||
});
|
||||
}
|
||||
|
||||
#closeMenu() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<DModalBody @title="discourse_ai.summarization.title">
|
||||
{{#if @allowTimeframe}}
|
||||
<span>{{i18n "discourse_ai.summarization.description"}}</span>
|
||||
<ComboBox
|
||||
@value={{this.sinceHours}}
|
||||
@content={{this.sinceOptions}}
|
||||
@onChange={{action this.summarize}}
|
||||
@valueProperty="value"
|
||||
@class="summarization-since"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<div class="channel-summary">
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
|
||||
{{#unless this.loading}}
|
||||
<Textarea @value={{this.summary}} disabled="true" class="summary-area" />
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#unless @allowTimeframe}}
|
||||
<DButton
|
||||
@class="btn-primary create"
|
||||
@action={{this.summarize}}
|
||||
@label="discourse_ai.summarization.summarize"
|
||||
/>
|
||||
{{/unless}}
|
||||
<DModalCancel @close={{route-action "closeModal"}} />
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class AiSummary extends Component {
|
||||
@tracked sinceHours = null;
|
||||
@tracked loading = false;
|
||||
@tracked availableSummaries = {};
|
||||
@tracked summary = null;
|
||||
sinceOptions = [
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 1 }),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 3 }),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 6 }),
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 12 }),
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 24 }),
|
||||
value: 24,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 72 }),
|
||||
value: 72,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 168 }),
|
||||
value: 168,
|
||||
},
|
||||
];
|
||||
|
||||
get canSummarize() {
|
||||
return (!this.args.allowTimeframe || this.sinceHours) && !this.loading;
|
||||
}
|
||||
|
||||
@action
|
||||
summarize(value) {
|
||||
this.loading = true;
|
||||
const attrs = {
|
||||
target_id: this.args.targetId,
|
||||
target_type: this.args.targetType,
|
||||
};
|
||||
|
||||
if (this.args.allowTimeframe) {
|
||||
this.sinceHours = value;
|
||||
|
||||
if (this.availableSummaries[this.sinceHours]) {
|
||||
this.summary = this.availableSummaries[this.sinceHours];
|
||||
this.loading = false;
|
||||
return;
|
||||
} else {
|
||||
attrs.since = this.sinceHours;
|
||||
}
|
||||
}
|
||||
|
||||
ajax("/discourse-ai/summarization/summary", {
|
||||
method: "POST",
|
||||
data: attrs,
|
||||
})
|
||||
.then((data) => {
|
||||
if (this.args.allowTimeframe) {
|
||||
this.availableSummaries[this.sinceHours] = data.summary;
|
||||
this.summary = this.availableSummaries[this.sinceHours];
|
||||
} else {
|
||||
this.summary = data.summary;
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => (this.loading = false));
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
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 {
|
||||
<template>
|
||||
<DModal
|
||||
class="composer-ai-helper-modal"
|
||||
@title={{t "discourse_ai.ai_helper.context_menu.changes"}}
|
||||
@closeModal={{@closeModal}}
|
||||
>
|
||||
<:body>
|
||||
{{#if @diff}}
|
||||
{{htmlSafe @diff}}
|
||||
{{else}}
|
||||
<div class="composer-ai-helper-modal__old-value">
|
||||
{{@oldValue}}
|
||||
</div>
|
||||
|
||||
<div class="composer-ai-helper-modal__new-value">
|
||||
{{@newValue}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</:body>
|
||||
|
||||
<:footer>
|
||||
<DButton
|
||||
class="btn-primary confirm"
|
||||
@action={{this.triggerConfirmChanges}}
|
||||
@label="discourse_ai.ai_helper.context_menu.confirm"
|
||||
/>
|
||||
<DModalCancel @close={{@closeModal}} />
|
||||
</:footer>
|
||||
</DModal>
|
||||
</template>
|
||||
|
||||
@action
|
||||
triggerConfirmChanges() {
|
||||
this.args.closeModal();
|
||||
this.args.confirm();
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
|
||||
import { inject as service } from "@ember/service";
|
||||
import showAIHelper from "../../lib/show-ai-helper";
|
||||
|
||||
|
||||
export default class AICategorySuggestion extends Component {
|
||||
<template>
|
||||
{{#if this.siteSettings.ai_embeddings_enabled}}
|
||||
<AISuggestionDropdown @mode="suggest_category" @composer={{@outletArgs.composer}} class="suggest-category-button"/>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
static shouldRender(outletArgs, helper) {
|
||||
return showAIHelper(outletArgs, helper);
|
||||
}
|
||||
|
||||
@service siteSettings;
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
|
||||
import { inject as service } from "@ember/service";
|
||||
import showAIHelper from "../../lib/show-ai-helper";
|
||||
|
||||
|
||||
export default class AITagSuggestion extends Component {
|
||||
<template>
|
||||
{{#if this.siteSettings.ai_embeddings_enabled}}
|
||||
<AISuggestionDropdown @mode="suggest_tags" @composer={{@outletArgs.composer}} class="suggest-tags-button"/>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
static shouldRender(outletArgs, helper) {
|
||||
return showAIHelper(outletArgs, helper);
|
||||
}
|
||||
|
||||
@service siteSettings;
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
|
||||
import showAIHelper from "../../lib/show-ai-helper";
|
||||
|
||||
export default class AITitleSuggestion extends Component {
|
||||
<template>
|
||||
<AISuggestionDropdown @mode="suggest_title" @composer={{@outletArgs.composer}} class="suggest-titles-button" />
|
||||
</template>
|
||||
|
||||
static shouldRender(outletArgs, helper) {
|
||||
return showAIHelper(outletArgs, helper);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
<div {{did-insert this.setupContextMenu}}>
|
||||
{{#if this.showContextMenu}}
|
||||
<div class="ai-helper-context-menu">
|
||||
{{#if (eq this.menuState this.CONTEXT_MENU_STATES.triggers)}}
|
||||
<ul class="ai-helper-context-menu__trigger">
|
||||
<li>
|
||||
<DButton
|
||||
@icon="discourse-sparkles"
|
||||
@action={{this.toggleAiHelperOptions}}
|
||||
@label="discourse_ai.ai_helper.context_menu.trigger"
|
||||
class="btn-flat"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{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="Enter a custom prompt..."
|
||||
class="ai-custom-prompt__input"
|
||||
@enter={{action (fn this.updateSelected option)}}
|
||||
/>
|
||||
|
||||
<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}}
|
||||
{{/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>
|
||||
|
||||
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.review)}}
|
||||
<ul class="ai-helper-context-menu__review">
|
||||
<li>
|
||||
<DButton
|
||||
@icon="exchange-alt"
|
||||
@label="discourse_ai.ai_helper.context_menu.view_changes"
|
||||
@action={{this.viewChanges}}
|
||||
class="btn-flat view-changes"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<DButton
|
||||
@icon="undo"
|
||||
@label="discourse_ai.ai_helper.context_menu.revert"
|
||||
@action={{this.undoAIAction}}
|
||||
class="btn-flat revert"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<DButton
|
||||
@icon="check"
|
||||
@label="discourse_ai.ai_helper.context_menu.confirm"
|
||||
@action={{this.confirmChanges}}
|
||||
class="btn-flat confirm"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.resets)}}
|
||||
<ul class="ai-helper-context-menu__resets">
|
||||
<li>
|
||||
<DButton
|
||||
@icon="undo"
|
||||
@label="discourse_ai.ai_helper.context_menu.undo"
|
||||
@action={{this.undoAIAction}}
|
||||
class="btn-flat undo"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<DButton
|
||||
@icon="discourse-sparkles"
|
||||
@label="discourse_ai.ai_helper.context_menu.regen"
|
||||
@action={{this.updateSelected}}
|
||||
@actionParam={{this.lastUsedOption}}
|
||||
class="btn-flat"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.showDiffModal}}
|
||||
<Modal::DiffModal
|
||||
@confirm={{this.confirmChanges}}
|
||||
@diff={{this.diff}}
|
||||
@oldValue={{this.selectedText}}
|
||||
@newValue={{this.newSelectedText}}
|
||||
@closeModal={{fn (mut this.showDiffModal) false}}
|
||||
/>
|
||||
{{/if}}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
{{#if this.isAiBotChat}}
|
||||
<DSection @bodyClass="ai-bot-chat" />
|
||||
{{#if this.renderChatWarning}}
|
||||
<div class="ai-bot-chat-warning">{{i18n
|
||||
"discourse_ai.ai_bot.pm_warning"
|
||||
}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@@ -1,32 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { computed } from "@ember/object";
|
||||
|
||||
export default class extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
get composerModel() {
|
||||
return this.args?.outletArgs?.composer;
|
||||
}
|
||||
|
||||
get renderChatWarning() {
|
||||
return this.siteSettings.ai_bot_enable_chat_warning;
|
||||
}
|
||||
|
||||
@computed("composerModel.targetRecipients")
|
||||
get isAiBotChat() {
|
||||
if (
|
||||
this.composerModel &&
|
||||
this.composerModel.targetRecipients &&
|
||||
this.currentUser.ai_enabled_chat_bots
|
||||
) {
|
||||
let reciepients = this.composerModel.targetRecipients.split(",");
|
||||
|
||||
return this.currentUser.ai_enabled_chat_bots.any((bot) =>
|
||||
reciepients.any((username) => username === bot.username)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
-51
@@ -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}}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import I18n from "I18n";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { isValidSearchTerm, translateResults } from "discourse/lib/search";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { SEARCH_TYPE_DEFAULT } from "discourse/controllers/full-page-search";
|
||||
|
||||
export default class extends Component {
|
||||
static shouldRender(_args, { siteSettings }) {
|
||||
return siteSettings.ai_embeddings_semantic_search_enabled;
|
||||
}
|
||||
|
||||
@service appEvents;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked searching = true;
|
||||
@tracked collapsedResults = true;
|
||||
@tracked results = [];
|
||||
|
||||
@computed("args.outletArgs.search")
|
||||
get searchTerm() {
|
||||
return this.args.outletArgs.search;
|
||||
}
|
||||
|
||||
@computed("args.outletArgs.type", "searchTerm")
|
||||
get searchEnabled() {
|
||||
return (
|
||||
this.args.outletArgs.type === SEARCH_TYPE_DEFAULT &&
|
||||
isValidSearchTerm(this.searchTerm, this.siteSettings)
|
||||
);
|
||||
}
|
||||
|
||||
@computed("results")
|
||||
get collapsedResultsTitle() {
|
||||
return I18n.t("discourse_ai.embeddings.semantic_search_results.toggle", {
|
||||
count: this.results.length,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.appEvents.on(
|
||||
"full-page-search:trigger-search",
|
||||
this,
|
||||
"debouncedSearch"
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
teardown() {
|
||||
this.appEvents.off(
|
||||
"full-page-search:trigger-search",
|
||||
this,
|
||||
"debouncedSearch"
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
performHyDESearch() {
|
||||
if (!this.searchEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.searching = true;
|
||||
this.collapsedResults = true;
|
||||
this.results = [];
|
||||
|
||||
ajax("/discourse-ai/embeddings/semantic-search", {
|
||||
data: { q: this.searchTerm },
|
||||
})
|
||||
.then(async (results) => {
|
||||
const model = (await translateResults(results)) || {};
|
||||
this.results = model.posts;
|
||||
})
|
||||
.finally(() => (this.searching = false));
|
||||
}
|
||||
|
||||
@action
|
||||
debouncedSearch() {
|
||||
discourseDebounce(this, this.performHyDESearch, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
shouldRender(args) {
|
||||
return (args.model.related_topics?.length || 0) > 0;
|
||||
},
|
||||
setupComponent(args, component) {
|
||||
if (component.model.related_topics) {
|
||||
component.set(
|
||||
"relatedTopics",
|
||||
component.model.related_topics.map((topic) =>
|
||||
this.store.createRecord("topic", topic)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
<div
|
||||
id="related-topics"
|
||||
class="more-topics__list {{if this.hidden 'hidden'}}"
|
||||
role="complementary"
|
||||
aria-labelledby="related-topics-title"
|
||||
{{did-insert this.registerList}}
|
||||
{{will-destroy this.removeList}}
|
||||
>
|
||||
<h3 id="related-topics-title" class="more-topics__list-title">
|
||||
{{i18n "discourse_ai.related_topics.title"}}
|
||||
</h3>
|
||||
<div class="topics">
|
||||
<BasicTopicList @topics={{this.relatedTopics}} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,41 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action, computed } from "@ember/object";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class extends Component {
|
||||
static shouldRender(args) {
|
||||
return (args.model.related_topics?.length || 0) > 0;
|
||||
}
|
||||
|
||||
@service store;
|
||||
@service site;
|
||||
@service moreTopicsPreferenceTracking;
|
||||
|
||||
listId = "related-topics";
|
||||
|
||||
@computed("moreTopicsPreferenceTracking.selectedTab")
|
||||
get hidden() {
|
||||
return this.moreTopicsPreferenceTracking.selectedTab !== this.listId;
|
||||
}
|
||||
|
||||
get relatedTopics() {
|
||||
return this.args.outletArgs.model.related_topics.map((topic) =>
|
||||
this.store.createRecord("topic", topic)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
registerList() {
|
||||
this.moreTopicsPreferenceTracking.registerTopicList({
|
||||
name: I18n.t("discourse_ai.related_topics.pill"),
|
||||
id: this.listId,
|
||||
icon: "magic",
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
removeList() {
|
||||
this.moreTopicsPreferenceTracking.removeTopicList(this.listId);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Composer from "discourse/models/composer";
|
||||
import I18n from "I18n";
|
||||
|
||||
export function composeAiBotMessage(targetBot, composer) {
|
||||
const currentUser = composer.currentUser;
|
||||
|
||||
let botUsername = currentUser.ai_enabled_chat_bots.find(
|
||||
(bot) => bot.model_name === targetBot
|
||||
).username;
|
||||
export async function composeAiBotMessage(targetBot, composer) {
|
||||
let botUsername = await ajax("/discourse-ai/ai-bot/bot-username", {
|
||||
data: { username: targetBot },
|
||||
}).then((data) => {
|
||||
return data.bot_username;
|
||||
});
|
||||
|
||||
composer.focusComposer({
|
||||
fallbackToNewTopic: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user