Compare commits

..
Author SHA1 Message Date
Sam Saffron 13fca098d1 tweaks to open llm implementation
testing mistral at the moment and did the minimum to get it going
2023-10-04 17:20:46 +11:00
Sam Saffron d76309fa17 tweaks to llama2 support in chat, fix message so it conforms to recommendation
by facebook (add <s> )
2023-10-04 16:27:00 +11:00
Sam Saffron facce51f98 FEATURE: add support for an open LLM implementation
This is very much a work in progress, testing at the moment on:

TheBloke/Llama-2-7B-chat-fp16
2023-10-04 16:25:44 +11:00
Rafael dos Santos Silva 4df258ce7d FIX: Follow fix for missing claude tags in a7adce0 (#242) 2023-10-03 19:56:29 -03:00
Rafael dos Santos Silva a7adce0cf7 FIX: Fallback to whole response when Claude forgets tags (#240) 2023-10-03 15:39:30 -03:00
SamandJoffrey JAFFEUX 0cbf14e343 FEATURE: automation rule for triaging posts using LLM (#236)
The new automation rule can be used to perform llm based classification and categorization of topics. 

You specify a system prompt (which has %%POST%% as an input), if it returns a particular piece of text then we will apply rules such as tagging, hiding, replying or categorizing.

This can be used as a spam filter, a "oops you are in the wrong place" filter and so on. 

Co-authored-by: Joffrey JAFFEUX <[email protected]>
2023-10-03 08:55:30 +11:00
Rafael dos Santos Silva 453928e7bb FIX: Improvment to embeddings index task (#238) 2023-10-02 16:37:13 -03:00
Rafael dos Santos Silva 9d96c4d242 FIX: Suggest category/tag was broken since 2e5a393 (#237) 2023-10-02 16:36:56 -03:00
Rafael dos Santos Silva 102f47c1c4 FEATURE: Allow Anthropic inference via AWS Bedrock (#235)
If a module LLM model is set to claude-2 and the ai_bedrock variables are all present we will use AWS Bedrock instead of Antrhopic own APIs.

This is quite hacky, but will allow us to test the waters with AWS Bedrock early access with every module.

This situation of "same module, completely different API" is quite a bit far from what we had in the OpenAI/Azure separation, so it's more food for thought for when we start working on the LLM abstraction layer soon this year.
2023-10-02 12:58:36 -03:00
Sam ed7d1f06d1 FIX: improve token counting (#234)
We were running out of tokens under certain conditions (really long
chains)

Add more buffer.
2023-09-28 15:32:22 +10:00
Keegan George 237e9478df UX: Use disable/enable state for custom prompt button (#233) 2023-09-27 14:27:16 -07:00
Keegan George 0c50d7d553 DEV: Improvements to context menu (#232) 2023-09-27 11:39:51 -07:00
Discourse Translator Bot 782600e64f Update translations (#229) 2023-09-27 11:03:11 +02:00
Sam aa463d64f1 FEATURE: Add creative persona (#231)
This adds a new creative persona that has access to the underlying
model and no external integrations.

It allows people to use Claude/GPT models in a Discourse agnostic
way.
2023-09-27 10:48:38 +10:00
Rafael dos Santos Silva 525be42aa5 FIX: AIHelper custom prompts with OpenAI had wrong type (#230) 2023-09-25 17:26:00 -03:00
Keegan GeorgeandRafael dos Santos Silva 2e5a39360a FEATURE: Create custom prompts with composer AI helper (#214)
* DEV: Add icon support

* DEV: Add basic setup for custom prompt menu

* FEATURE: custom prompt backend

* fix custom prompt param check

* fix custom prompt replace

* WIP

* fix custom prompt usage

* fixes

* DEV: Update front-end

* DEV: No more custom prompt state

* DEV: Add specs

* FIX: Title/Category/Tag suggestions

Suggestion dropdowns broke because it `messages_with_user_input(user_input)` expects a hash now.

* DEV: Apply syntax tree

* DEV: Restrict custom prompts to configured groups

* oops

* fix tests

* lint

* I love tests

* lint is cool tho

---------

Co-authored-by: Rafael dos Santos Silva <[email protected]>
2023-09-25 15:12:54 -03:00
Sam 316ea9624e FIX: properly truncate !command prompts (#227)
* FIX: properly truncate !command prompts

### What is going on here?

Previous to this change where a command was issued by the LLM it
could hallucinate a continuation eg:

```
This is what tags are

!tags

some nonsense here
```

This change introduces safeguards so `some nonsense here` does not
creep in to the prompt history, poisoning the llm results

This in effect grounds the llm a lot better and results in the llm
forgetting less about results.

The change only impacts Claude at the moment, but will also improve
stuff for llama 2 in future.

Also, this makes it significantly easier to test the bot framework
without an llm cause we avoid a whole bunch of complex stubbing

* blank is not a valid bot response, do not inject into prompt
2023-09-15 07:02:37 +10:00
Roman Rizzi f57c1bb0f6 FEATURE: AI Helper endpoint to generate a thumbnail from text. (#224)
We pass the text to the current LLM and ask them to generate a StableDifussion prompt.
We'll use that to generate 4 samples, temporarily creating uploads and returning their short URLs.
2023-09-14 12:53:44 -03:00
Jarek Radosz 1eb70c4f0a DEV: Fix rspec-expectations warnings (#228) 2023-09-14 17:50:13 +02:00
SamandAlan Guo Xiang Tan 9e94457154 FIX: Made bot more robust (#226)
* FIX: Made bot more robust

This is a collection of small fixes

- Display "Searching for: ..." while searching instead of showing found 0 results.
- Only allow 5 commands in lang chain - 6 feels like too much
- On the 5th command stop informing the engine about functions, so it is forced to complete
- Add another 30 tokens of buffer and explain why
- Typo in command prompt


Co-authored-by: Alan Guo Xiang Tan <[email protected]>
2023-09-14 16:46:56 +10:00
Keegan George d35c8d5eca FIX: Conditionally render AI suggestion buttons (#225) 2023-09-13 14:18:48 -07:00
Keegan George cf6a6ed9b8 FIX: AI Helper UX bugs (#223) 2023-09-13 09:53:19 -07:00
Sam cdd6faa648 FEATURE: add filter support to ai bot semantic search (#222)
Previously we would bypass semantic search if any filters were
present

Also shows progress now.
2023-09-13 14:59:45 +10:00
Rafael dos Santos Silva d1642533fb FIX: Use "Related Topics" label consistently (#221) 2023-09-12 16:23:24 -03:00
Rafael dos Santos Silva 2c87bb0d99 FEATURE: Respect search filters in semantic search (#220)
* FEATURE: Respect search filters in semantic search

* lint
2023-09-12 16:16:33 -03:00
Roman Rizzi 50d7289623 FIX: Trigger initial semantic search. (#219)
I thought this wasn't neccessary and we could safely rely on the appEvent during the initial search.
It only fires if #searchEnabled is true, meaning the search term is valid.
2023-09-12 10:28:07 -03:00
Discourse Translator Bot 0d761f4305 Update translations (#218) 2023-09-12 15:27:58 +02:00
Sam d75e3ca82b FEATURE: include tag and category context in search (#217)
Previous to this we just included title/body.. tags and category
structure can be very critical for decision making.
2023-09-12 16:09:28 +10:00
Sam b0310f90d3 FEATURE: add tags and categories to read context (#215)
Note, we perform permission checks on tag list against anon
to ensure we do not disclose information about private tags
to the llm which could get extracted.
2023-09-12 11:06:55 +10:00
Roman Rizzi 156931e1f4 FIX: Perform semantic search only when searchTerm is valid (#216) 2023-09-11 11:32:05 -03:00
Keegan George 6295b16678 FEATURE: Add cancel button to loading context menu (#213) 2023-09-07 13:50:56 -07:00
Keegan George 087be9f4da UX: Prevent input text from covering suggestion button (#212) 2023-09-07 11:52:00 -07:00
Roman Rizzi 0828254d61 FIX: Generate embeddings job was broken (#211)
* FIX: Use correct methods to generate embeddings

* FIX: Generate embeddings job was broken
2023-09-07 11:54:43 -03:00
Sam 615eb8b440 FEATURE: add semantic search with hyde bot (#210)
In specific scenarios (no special filters or limits) we will also
always include 5 semantic results (at least) with every query.

This effectively means that all very wide queries will always return
20 results, regardless of how complex they are.

Also: 

FIX: embedding backfill rake task not working
We renamed internals, this corrects the implementation
2023-09-07 13:25:26 +10:00
Keegan George abe96d5533 DEV: Strip out old modal based AI helper (#209) 2023-09-06 13:28:47 -07:00
Keegan George 0733ff7e67 UX: Show suggestion buttons only if sufficient content is present (#204) 2023-09-06 12:20:08 -07:00
Keegan George 8d674c451a FIX: Flaky spec in AI Helper modal (#208) 2023-09-06 10:15:11 -07:00
Keegan George aa08f2d2a0 FIX: Flaky Spec (#207) 2023-09-06 09:46:03 -07:00
Rafael dos Santos Silva 5c50d2aa09 FEATURE: Use stop_sequences for faster HyDE searches with Claude (#203) 2023-09-06 10:06:31 -03:00
Roman Rizzi 13d63f1f30 FIX: filter allowed categories from semantic search results (#206) 2023-09-06 10:00:20 -03:00
Alan Guo Xiang Tan 920d4d8c0c DEV: Skip broken test on CI (#205) 2023-09-06 09:33:43 +08:00
Roman Rizzi 4d854e9232 FIX: Invalidate semantic search cache entries when hyde or embedding model changes (#202) 2023-09-05 18:39:39 -03:00
Keegan George 3a00c38fc5 UX: Change AI Helper toggle icon to sparkles (#201) 2023-09-05 13:21:05 -07:00
Rafael dos Santos Silva 4b42c09814 FEATURE: Tweak HyDE prompts for better grounding in forum subject and limit response size (#200)
* FEATURE: Tweak HyDE prompts for better grounding in forum subject and limit response size

* fix test

* lint
2023-09-05 16:11:07 -03:00
Roman Rizzi 175def1267 FIX: Update migrations with latest vector rep changes (#199) 2023-09-05 14:31:04 -03:00
Rafael dos Santos Silva ee734a340a FIX: Tag/category suggestion broke in 2c0f535 (#198) 2023-09-05 14:15:01 -03:00
Keegan George ae0238c616 FIX: Flaky spec (#197) 2023-09-05 09:56:12 -07:00
Rafael dos Santos SilvaandRoman Rizzi 2c0f535bab FEATURE: HyDE-powered semantic search. (#136)
* FEATURE: HyDE-powered semantic search.

It relies on the new outlet added on discourse/discourse#23390 to display semantic search results in an unobtrusive way.

We'll use a HyDE-backed approach for semantic search, which consists on generating an hypothetical document from a given keywords, which gets transformed into a vector and used in a asymmetric similarity topic search.

This PR also reorganizes the internals to have less moving parts, maintaining one hierarchy of DAOish classes for vector-related operations like transformations and querying.

Completions and vectors created by HyDE will remain cached on Redis for now, but we could later use Postgres instead.

* Missing translation and rate limiting

---------

Co-authored-by: Roman Rizzi <[email protected]>
2023-09-05 11:08:23 -03:00
Discourse Translator Bot 3d83d062a1 Update translations (#186) 2023-09-05 15:42:46 +02:00
Sam 38af2ca63e FIX: cut completion short after function call is found (#182)
Previous to this change we would keep completing and throw away
result
2023-09-05 10:37:58 +10:00
Rafael dos Santos Silva 86591cf1d2 CHORE: Update Readme (#185) 2023-09-04 15:46:35 -03:00
Rafael dos Santos Silva 4864978495 FEATURE: Return only applicable suggestions in AiHelper category/tags suggestions (#184) 2023-09-04 14:30:33 -03:00
Rafael dos Santos Silva 3c4a53b2cb FEATURE: Better link in Claude summaries (#183)
* FEATURE: Better link in Claude summaries

* lint
2023-09-04 12:04:47 -03:00
Sam e3abbd9f46 FEATURE: add researcher persona (#181)
The researcher persona has access to Google and can perform
various internet research tasks. At the moment it can not read
web pages, but that is under consideration
2023-09-04 12:05:27 +10:00
Sam 3f9973586e FIX: ai_bot_allowed_groups now works with restricted visibility (#180)
Previous to this change we relied on client side settings to
determine if an end user has access to the ai bot.

This meant that if a user was not aware they are a member of a
group (as it is with restricted visibility ones) they would not
see the bot button.

All checking has now moved to the server side, and tests were
added to cover.
2023-09-04 11:52:44 +10:00
Rafael dos Santos Silva 43e485cbd9 FEATURE: Additional AI suggestion options (#176) 2023-09-01 17:10:58 -07:00
SamandMartin Brennan 181113159b FIX: setting explorer was exceeding token budget
This refactor changes it so we only include minimal data in the
system prompt which leaves us lots of tokens for specific searches

The new search command allows us to pull in settings on demand

Descriptions are include in short search results, and names only
in longer results

Also: 

* In dev it is important to tell when calls are made to open ai
this adds a console log to increase awareness around token usage

* PERF: stop counting tokens so often

This changes it so we only count tokens once per response

Previously each time we heard back from open ai we would count
tokens, leading to uneeded delays

* bug fix, commands may reach in for tokenizer

* add logging to console for anthropic calls as well

* Update lib/shared/inference/openai_completions.rb

Co-authored-by: Martin Brennan <[email protected]>
2023-09-01 11:48:51 +10:00
Loïc Guitaut 65091690eb DEV: Don’t use Chat::MessageCreator in specs
As message creation is being rewritten in
https://github.com/discourse/discourse/pull/22390, a new way of using
the underlying service to create chat messages has been implemented in
https://github.com/discourse/discourse/pull/23222.

This patch uses the new fabricator option which will prevent breaking
specs from this plugin when the main PR will be merged.
2023-08-31 11:30:07 +02:00
Sam 00d69b463e FEATURE: new site setting explorer persona (#178)
Also adds ai_bot_enabled_personas so admins can tweak which stock
personas are enabled.

The new persona has a full listing of all site settings and is
able to get context for each setting.

This means you can ask it to search through settings for something
relevant.

Security wise there is no access to actual configuration of settings
just to the names / description and implementation.

Previously this was part of the forum helper persona however it
just clashes too much with other behaviors, isolating it makes
it far more powerful.

* sneaking this one in, user_emails is a non obvious table in our
structure.

usually one would assume users has emails so the clarifies a bit
better. plus it is a very common table to hit.
2023-08-31 17:02:03 +10:00
Sam 8e4347acba DEV: rename ai_helper_add_ai_pm_to_header -> ai_bot_add_to_header (#177)
Old name was very unclear, this setting is only used for the bot
so now it follows the same convention others do
2023-08-31 14:42:28 +10:00
Sam db19e37748 FEATURE: add initial support for personas (#172)
This splits out a bunch of code that used to live inside bots
into a dedicated concept called a Persona.

This allows us to start playing with multiple personas for the bot

Ships with:

artist - for making images
sql helper - for helping with data explorer
general - for everything and anything
 
Also includes a few fixes that make the generic LLM function implementation  more robust
2023-08-30 16:15:03 +10:00
Rafael dos Santos Silva 6d69fb479e DEV: Hide old embeddings pg setting (#169)
* DEV: Hide old embeddings pg setting

* fix yaml
2023-08-29 17:39:21 -03:00
Keegan George 4da4b5609f FIX: Show warning when trying to generate suggestions without content (#175) 2023-08-29 11:58:45 -07:00
Keegan George 494964c51d DEV: Dynamically set popper placement (#174) 2023-08-29 11:19:25 -07:00
Keegan George 7457feced8 FEATURE: Show suggested title prompt in new location (#171) 2023-08-29 09:45:53 -07:00
Discourse Translator Bot 345bfed19f Update translations (#173) 2023-08-29 15:51:02 +02:00
Sam 8fdb88604f FIX: trim first space when getting a reply from anthropic (#164)
Anthropic loves sending a pointless leading space with completions
this throws off the command framework.
2023-08-29 10:57:36 +10:00
Sam b14cb864dc FEATURE: add setting_context experimental command (#160)
This command can be used to extract information about a discourse
site setting directly from source.

To operate it needs the rg binary in the container.
2023-08-29 10:43:58 +10:00
Keegan George fba419f864 UX: Clicking outside editor should close context menu (#170) 2023-08-28 15:08:51 -07:00
Keegan George 2c50791f26 UX: Prevent context menu overflow outside of container (#168) 2023-08-28 13:31:21 -07:00
Keegan George b470692409 DEV: Preload prompt list on component initialization (#165) 2023-08-28 12:22:44 -07:00
Keegan George 6e01b2c334 UX: Improve styling and make button full width of context menu (#167) 2023-08-28 12:22:32 -07:00
Keegan George 657b2d681a DEV: Update ESLint config version (#166) 2023-08-28 12:21:52 -07:00
Rafael dos Santos Silva 513c51dcb2 FIX: Renumber StableBeluga2 AiHelper prompts to fix conflict (#163) 2023-08-25 17:19:50 -03:00
Rafael dos Santos Silva e673b568d9 FEATURE: StableBeluga2 support for AiHelper (#162)
* FEATURE: StableBeluga2 support for AiHelper

* lint
2023-08-25 15:54:51 -03:00
195 changed files with 6237 additions and 2328 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# **Plugin Name** Plugin
# **Discourse AI** Plugin
**Plugin Summary**
For more information, please see: **url to meta topic**
For more information, please see: https://meta.discourse.org/t/discourse-ai/259214?u=falco
@@ -6,6 +6,7 @@ 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:
@@ -17,29 +18,94 @@ module DiscourseAi
end
def suggest
raise Discourse::InvalidParameters.new(:text) if params[:text].blank?
input = get_text_param!
prompt = CompletionPrompt.find_by(id: params[:mode])
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
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[:text],
),
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?
hijack do
render json:
DiscourseAi::AiHelper::LlmPrompt.new.generate_and_send_prompt(
prompt,
input_hash,
),
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,8 +8,11 @@ module DiscourseAi
SEMANTIC_SEARCH_TYPE = "semantic_search"
def search
query = params[:q]
page = (params[:page] || 1).to_i
query = params[:q].to_s
if query.length < SiteSetting.min_search_term_length
raise Discourse::InvalidParameters.new(:q)
end
grouped_results =
Search::GroupedSearchResults.new(
@@ -19,12 +22,19 @@ module DiscourseAi
use_pg_headlines_for_excerpt: false,
)
DiscourseAi::Embeddings::SemanticSearch
.new(guardian)
.search_for_topics(query, page)
.each { |topic_post| grouped_results.add(topic_post) }
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
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
end
end
end
+18 -4
View File
@@ -10,10 +10,24 @@ class CompletionPrompt < ActiveRecord::Base
validate :each_message_length
def messages_with_user_input(user_input)
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>" }
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])
end
end
@@ -1,69 +0,0 @@
<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>
@@ -1,152 +0,0 @@
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();
}
}
@@ -0,0 +1,196 @@
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;
}
}
@@ -39,7 +39,7 @@ export default class ModalDiffModal extends Component {
</:footer>
</DModal>
</template>
@action
triggerConfirmChanges() {
this.args.closeModal();
@@ -0,0 +1,19 @@
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;
}
@@ -0,0 +1,19 @@
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;
}
@@ -0,0 +1,13 @@
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);
}
}
@@ -5,7 +5,7 @@
<ul class="ai-helper-context-menu__trigger">
<li>
<DButton
@icon="magic"
@icon="discourse-sparkles"
@action={{this.toggleAiHelperOptions}}
@label="discourse_ai.ai_helper.context_menu.trigger"
class="btn-flat"
@@ -16,40 +16,53 @@
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.options)}}
<ul class="ai-helper-context-menu__options">
{{#each this.helperOptions as |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>
{{#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)}}
/>
{{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>
<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)}}
<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>
<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">
@@ -99,7 +112,6 @@
/>
</li>
</ul>
{{/if}}
</div>
{{/if}}
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,6 @@ import { inject as service } from "@ember/service";
import { computed } from "@ember/object";
export default class extends Component {
static shouldRender() {
return true;
}
@service currentUser;
@service siteSettings;
@@ -0,0 +1,7 @@
<div class="gpt-persona">
<DropdownSelectBox
@value={{this.value}}
@content={{this.botOptions}}
@options={{hash icon="robot"}}
/>
</div>
@@ -0,0 +1,53 @@
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 };
}
}
@@ -0,0 +1,51 @@
{{#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}}
@@ -0,0 +1,86 @@
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,20 @@
export default function showAIHelper(outletArgs, helper) {
const helperEnabled =
helper.siteSettings.discourse_ai_enabled &&
helper.siteSettings.composer_ai_helper_enabled;
const allowedGroups = helper.siteSettings.ai_helper_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
const canUseAssistant = helper.currentUser?.groups.some((g) =>
allowedGroups.includes(g.id)
);
const canShowInPM = helper.siteSettings.ai_helper_allowed_in_pm;
if (outletArgs?.composer?.privateMessage) {
return helperEnabled && canUseAssistant && canShowInPM;
}
return helperEnabled && canUseAssistant;
}
@@ -1 +0,0 @@
<AiHelper @editor={{this.editor}} @closeModal={{route-action "closeModal"}} />
@@ -4,15 +4,17 @@ import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import loadScript from "discourse/lib/load-script";
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import { hbs } from "ember-cli-htmlbars";
function isGPTBot(user) {
return user && [-110, -111, -112].includes(user.id);
return user && [-110, -111, -112, -113].includes(user.id);
}
function attachHeaderIcon(api) {
const settings = api.container.lookup("service:site-settings");
const enabledBots = settings.ai_helper_add_ai_pm_to_header
const enabledBots = settings.ai_bot_add_to_header
? settings.ai_bot_enabled_chat_bots.split("|").filter(Boolean)
: [];
if (enabledBots.length > 0) {
@@ -138,25 +140,45 @@ function initializeAIBotReplies(api) {
});
}
function initializePersonaDecorator(api) {
let topicController = null;
api.decorateWidget(`poster-name:after`, (dec) => {
if (!isGPTBot(dec.attrs.user)) {
return;
}
// this is hacky and will need to change
// trouble is we need to get the model for the topic
// and it is not available in the decorator
// long term this will not be a problem once we remove widgets and
// have a saner structure for our model
topicController =
topicController || api.container.lookup("controller:topic");
return dec.widget.attach("persona-flair", {
topicController,
});
});
registerWidgetShim(
"persona-flair",
"span.persona-flair",
hbs`{{@data.topicController.model.ai_persona_name}}`
);
}
export default {
name: "discourse-ai-bot-replies",
initialize(container) {
const settings = container.lookup("service:site-settings");
const user = container.lookup("service:current-user");
const aiBotEnaled =
settings.discourse_ai_enabled && settings.ai_bot_enabled;
const aiBotsAllowedGroups = settings.ai_bot_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
const canInteractWithAIBots = user?.groups.some((g) =>
aiBotsAllowedGroups.includes(g.id)
);
if (aiBotEnaled && canInteractWithAIBots) {
withPluginApi("1.6.0", attachHeaderIcon);
if (user?.ai_enabled_chat_bots) {
if (settings.ai_bot_add_to_header) {
withPluginApi("1.6.0", attachHeaderIcon);
}
withPluginApi("1.6.0", initializeAIBotReplies);
withPluginApi("1.6.0", initializePersonaDecorator);
}
},
};
@@ -1,72 +0,0 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import showModal from "discourse/lib/show-modal";
function initializeComposerAIHelper(api) {
api.modifyClass("component:composer-editor", {
pluginId: "discourse-ai",
actions: {
extraButtons(toolbar) {
this._super(toolbar);
const removeAiHelperFromPM =
this.composerModel.privateMessage &&
!this.siteSettings.ai_helper_allowed_in_pm;
if (removeAiHelperFromPM) {
const extrasGroup = toolbar.groups.find((g) => g.group === "extras");
const newButtons = extrasGroup.buttons.filter(
(b) => b.id !== "ai-helper"
);
extrasGroup.buttons = newButtons;
}
},
},
});
api.modifyClass("component:d-editor", {
pluginId: "discourse-ai",
actions: {
openAIHelper() {
if (this.value) {
showModal("composer-ai-helper").setProperties({ editor: this });
}
},
},
});
api.onToolbarCreate((toolbar) => {
toolbar.addButton({
id: "ai-helper",
title: "discourse_ai.ai_helper.title",
group: "extras",
icon: "discourse-sparkles",
className: "composer-ai-helper",
sendAction: () => toolbar.context.send("openAIHelper"),
});
});
}
export default {
name: "discourse-ai-composer-helper",
initialize(container) {
const settings = container.lookup("service:site-settings");
const user = container.lookup("service:current-user");
const helperEnabled =
settings.discourse_ai_enabled && settings.composer_ai_helper_enabled;
const allowedGroups = settings.ai_helper_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
const canUseAssistant = user?.groups.some((g) =>
allowedGroups.includes(g.id)
);
if (helperEnabled && canUseAssistant) {
withPluginApi("1.6.0", initializeComposerAIHelper);
}
},
};
@@ -1,63 +0,0 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import { translateResults, updateRecentSearches } from "discourse/lib/search";
import { ajax } from "discourse/lib/ajax";
const SEMANTIC_SEARCH = "semantic_search";
function initializeSemanticSearch(api) {
api.addFullPageSearchType(
"discourse_ai.embeddings.semantic_search",
SEMANTIC_SEARCH,
(searchController, args) => {
if (searchController.currentUser) {
updateRecentSearches(searchController.currentUser, args.searchTerm);
}
ajax("/discourse-ai/embeddings/semantic-search", { data: args })
.then(async (results) => {
const model = (await translateResults(results)) || {};
if (results.grouped_search_result) {
searchController.set("q", results.grouped_search_result.term);
}
if (args.page > 1) {
if (model) {
searchController.model.posts.pushObjects(model.posts);
searchController.model.topics.pushObjects(model.topics);
searchController.model.set(
"grouped_search_result",
results.grouped_search_result
);
}
} else {
model.grouped_search_result = results.grouped_search_result;
searchController.set("model", model);
}
searchController.set("error", null);
})
.catch((e) => {
searchController.set("error", e.jqXHR.responseJSON?.message);
})
.finally(() => {
searchController.setProperties({
searching: false,
loading: false,
});
});
}
);
}
export default {
name: "discourse-ai-full-page-semantic-search",
initialize(container) {
const settings = container.lookup("service:site-settings");
const semanticSearch = settings.ai_embeddings_semantic_search_enabled;
if (settings.ai_embeddings_enabled && semanticSearch) {
withPluginApi("1.6.0", initializeSemanticSearch);
}
},
};
@@ -2,9 +2,16 @@ nav.post-controls .actions button.cancel-streaming {
display: none;
}
.ai-bot-chat #reply-control {
.title-and-category {
display: none;
.ai-bot-chat {
#reply-control {
.title-and-category,
#private-message-users {
display: none;
}
}
.gpt-persona {
margin-bottom: 5px;
margin-top: -10px;
}
}
@@ -39,3 +46,9 @@ article.streaming nav.post-controls .actions button.cancel-streaming {
}
}
}
.topic-body .persona-flair {
order: 2;
font-size: var(--font-down-1);
padding-top: 3px;
}
@@ -1,10 +1,4 @@
.composer-ai-helper-modal {
.combobox,
.text-preview,
.ai-helper-waiting-selection {
margin: 10px 0 10px 0;
}
.text-preview,
.inline-diff {
ins {
@@ -31,11 +25,6 @@
background-color: var(--success-low);
color: var(--success);
}
.selection-hint {
font-size: var(--font-down-2);
margin-bottom: 20px;
}
}
.topic-above-suggested-outlet.related-topics {
@@ -44,46 +33,68 @@
.ai-helper-context-menu {
background: var(--secondary);
box-shadow: var(--shadow-dropdown);
box-shadow: var(--shadow-card);
padding: 0.25rem;
max-width: 25rem;
border: 1px solid var(--primary-low);
list-style: none;
z-index: 999;
&.out-of-bounds {
visibility: hidden;
pointer-events: none;
}
ul {
margin: 0;
list-style: none;
}
ul:not(.ai-helper-context-menu__loading) li {
transition: background-color 0.25s ease;
&:hover {
background: var(--primary-low);
}
}
li {
.btn-flat {
justify-content: left;
text-align: left;
background: none;
width: 100%;
border-radius: 0;
margin: 0;
padding-block: 0.6rem;
.d-button-label {
color: var(--primary-very-high);
&:focus,
&:hover {
color: var(--primary);
background: var(--d-hover);
.d-icon {
color: var(--primary-medium);
}
}
.d-button-label {
color: var(--primary-very-high);
}
}
}
&__options {
padding: 0.25rem;
li:not(:last-child) {
border-bottom: 1px solid var(--primary-low);
}
}
&__loading {
display: flex;
padding: 0.5rem;
gap: 1rem;
justify-content: flex-start;
align-items: center;
.dot-falling {
margin-inline: 1rem;
margin-left: 1.5rem;
}
li {
display: flex;
padding: 0.5rem;
gap: 1rem;
justify-content: flex-start;
align-items: center;
}
}
&__resets {
@@ -97,6 +108,41 @@
align-items: center;
flex-flow: row wrap;
}
&__custom-prompt {
display: flex;
flex-flow: row wrap;
padding: 0.5rem;
&-header {
margin-bottom: 0.5rem;
.btn {
padding: 0;
}
}
.ai-custom-prompt-input {
min-height: 90px;
width: 100%;
}
}
.ai-custom-prompt {
display: flex;
gap: 0.25rem;
margin-bottom: 0.5rem;
&__input {
background: var(--primary-low);
border-color: var(--primary-low);
margin-bottom: 0;
&::placeholder {
color: var(--primary-medium);
}
}
}
}
.d-editor-input.loading {
@@ -193,3 +239,83 @@
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
}
}
// Suggest Titles Related
.suggest-titles-button {
display: block;
align-self: baseline;
background: var(--secondary);
border: 1px solid var(--primary-medium);
border-left: none;
}
.title-input:has(.suggestion-button) {
// border on focus should be on top of suggestion button
input:focus {
z-index: 1;
}
}
.suggestion-button {
.d-icon-spinner {
animation: spin 1s linear infinite;
}
}
.suggest-tags-button,
.suggest-category-button {
display: block;
align-self: baseline;
border: 1px solid var(--primary-medium);
border-left: none;
background: none;
}
.ai-suggestions-menu {
list-style: none;
margin-left: 0;
position: absolute;
right: 0;
top: 1.5rem;
max-width: 25rem;
width: unset;
z-index: 999;
&__errors {
background: var(--danger);
padding: 0.25rem 1em;
color: var(--secondary);
}
}
.category-input:has(.ai-suggestions-menu) {
position: relative;
}
// Prevent suggestion button from wrapping on smaller screens
@media screen and (max-width: 768px) {
#reply-control {
.category-input:has(.suggestion-button) {
.category-chooser {
width: 10px;
}
+ .mini-tag-chooser {
width: 10px;
}
}
}
}
.suggest-tags-button + .ai-suggestions-menu {
top: 4.25rem;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
@@ -0,0 +1,39 @@
.semantic-search__container {
background: var(--primary-very-low);
margin: 1rem 0 1rem 0;
.semantic-search__results {
display: flex;
flex-direction: column;
align-items: baseline;
.semantic-search {
&__searching-text {
display: inline-block;
margin-left: 3px;
}
&__indicator-wave {
flex: 0 0 auto;
display: inline-flex;
}
&__indicator-dot {
display: inline-block;
animation: ai-summary__indicator-wave 1.8s linear infinite;
&:nth-child(2) {
animation-delay: -1.6s;
}
&:nth-child(3) {
animation-delay: -1.4s;
}
}
}
.semantic-search__entries {
margin-top: 10px;
}
.semantic-search__searching {
margin-left: 5px;
}
}
}
+7 -1
View File
@@ -10,12 +10,18 @@ ar:
modals:
select_option: "حدِّد خيارًا..."
related_topics:
title: "موضوعات مشابهة"
pill: "موضوعات ذات صلة"
ai_helper:
title: "اقتراح تغييرات باستخدام الذكاء الاصطناعي"
description: "اختر أحد الخيارات أدناه، وسيقترح عليك الذكاء الاصطناعي نسخة جديدة من النص."
selection_hint: "تلميح: يمكنك أيضًا تحديد جزء من النص قبل فتح أداة المساعدة لإعادة كتابة ذلك الجزء فقط."
context_menu:
back: "الرجوع"
undo: "تراجع"
cancel: "إلغاء"
regen: "إعادة المحاولة"
confirm: "تأكيد"
revert: "عودة"
reviewables:
model_used: "النموذج المستخدم:"
accuracy: "الدقة:"
+9
View File
@@ -5,3 +5,12 @@
# https://translate.discourse.org/
be:
js:
discourse_ai:
ai_helper:
context_menu:
back: "Назад"
undo: "Адмяніць"
cancel: "адмяніць"
regen: "Паспрабаваць яшчэ раз"
revert: "Вярнуць"
+9
View File
@@ -5,3 +5,12 @@
# https://translate.discourse.org/
bg:
js:
discourse_ai:
ai_helper:
context_menu:
back: "Назад"
undo: "Отмени"
cancel: "Прекрати"
regen: "Опитайте отново"
revert: "Върни"
+9
View File
@@ -5,3 +5,12 @@
# https://translate.discourse.org/
bs_BA:
js:
discourse_ai:
ai_helper:
context_menu:
back: "Prethodno"
undo: "Vrati nazad"
cancel: "Odustani"
regen: "Pokušaj ponovo"
revert: "Vrati nazad"

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