Compare commits

...
Author SHA1 Message Date
Jordan Vidrine 47fdb1acbf FIX: Adjust for overlap in AI buttons 2023-11-04 12:48:46 -05:00
Keegan George f6996a6ef0 DEV: Add wrapping classes and remove has() usage (#281) 2023-11-03 11:41:57 -07:00
David Taylor 0902f74af5 DEV: Update linting configs (#280) 2023-11-03 11:30:09 +00:00
Discourse Translator Bot e30082dd20 Update translations (#277) 2023-11-01 12:27:29 -03:00
Sam fc65404896 FEATURE: support topic_id and post_id logging in ai audit log (#274)
This makes it easier to track who is responsible for a completion
in logs

Note: ai helper and summarization are not yet implemented
2023-11-01 08:41:31 +11:00
chapoi c6d5c56033 UX: update border on AI topic helpers (#276)
Followup to e2d9117
2023-10-31 13:53:40 +01:00
Sam 0b62c0fa02 FIX: keep parity of shape for image command (#275)
Function calling will start hallucinating if you reshape results.

Previously we were morphing from:

`{ prompts: ["prompt 1", "prompt 2"] }`

to

`{ prompts: { prompt: "prompt 1", seed: 222}, { ... `

This meant that over a few call sequences function_call starts hallucinating an incorrect shape.

This change grounds us even on GPT-3.5
2023-10-31 19:12:25 +11:00
Rafael dos Santos Silva 6f708726e4 PERF: Better chat thread content format for LLM (#273) 2023-10-30 19:57:46 -03:00
Rafael dos Santos Silva cbfd8507b1 FIX: Update bedrock endpoint (#272)
* FIX: Update bedrock endpoint

AWS updated their endpoints per https://docs.aws.amazon.com/general/latest/gr/bedrock.html

* lint
2023-10-30 19:27:50 -03:00
Rafael dos Santos Silva 0c9e18799c FIX: unexpected return in aihelper entry_point (#271) 2023-10-30 13:32:56 -03:00
Rafael dos Santos Silva 3c55ea8fc0 FEATURE: Automatic Chat Thread titles (#269)
* FEATURE: Automatic Chat Thread titles

* do not gen title for empty threads

* make it default disabled for now
2023-10-30 11:56:33 -03:00
Sam b06380d9fa FIX: avoid semicolons at the end of queries for SQL Helper (#268)
This makes it easier to cut and paste snippets it is producing

Also fine tune the prompt in an attempt to hone gpt 3.5 which is very finicky
2023-10-27 16:21:09 +11:00
Sam 6add06af8f FEATURE: Make artist more creative (#266)
This allows for 2 big features:

1. Artist can ship up to 4 prompts for image generation
2. Artist can regenerate images cause it is aware of seed

This allows for iteration on images maintaining visual style
2023-10-27 14:48:12 +11:00
Rafael dos Santos Silva 818b20fb6f FEATURE: Make embeddings turn-key (#261)
To ease the administrative burden of enabling the embeddings model, this change introduces automatic backfill when the setting is enabled. It also moves the topic visit embedding creation to a lower priority queue in sidekiq and adds an option to skip embedding computation and persistence when we match on the digest.
2023-10-26 12:07:37 -03:00
Sam 426e348c8a FIX: make stable diffusion multi site friendly (#265)
Previous to this change image generation did not work on multisite

There was a background thread generating the images and it was
getting site settings from the default site in the cluster

This also removes referer header which is not needed
2023-10-25 11:04:16 +11:00
Gerhard Schlager b02be91799 UX: Replace hardcoded string with translation (#264) 2023-10-24 15:54:37 +02:00
Discourse Translator Bot 06c1356d86 Update translations (#263) 2023-10-24 15:53:44 +02:00
Sam 683f8fd4cc FIX: openai prompt was incorrect for explain (#262)
There is no "Human" role in openai, the blank assistant is not
needed
2023-10-24 16:36:28 +11:00
0e5764617a FEATURE: AI helper on posts (#244)
Adds an AI Helper function when selecting text while viewing a topic.

---------

Co-authored-by: Keegan George <[email protected]>
Co-authored-by: Roman Rizzi <[email protected]>
2023-10-23 11:41:36 -03:00
David Battersby cda5cb6e9c DEV: update test to use new chat message service (#254)
Update test to use the new chat update message service when editing chat messages.
2023-10-23 17:50:25 +08:00
David Battersby abd3da3383 DEV: skip edit message test until new service added to chat (#259)
Temporarily skip the edit chat message test as we are moving MessageUpdate to a new service in Chat.
2023-10-23 15:30:12 +08:00
Sam 1500308437 FEATURE: defer creation of bot users (#258)
Also fixes it so users without bot in header can send it messages.

Previous to this change we would seed all bots with database seeds.

This lead to lots of confusion for people who do not enable ai bot.

Instead:

1. We do not seed any bots **until** user enables the ai_bot_enabled setting
2. If it is disabled we will
  a. If no messages were created by bot - delete it
  b. Otherwise we will deactivate account
2023-10-23 17:00:58 +11:00
Ty Correll 87c591bbc2 UX: unify ai representing icon (#257)
This PR addresses the effort to use one icon representing discourse-ai.

Removed discourse-sparkles from discourse-ai, now included in core ``vendor/assets/svg-icons/discourse-additional.svg``
2023-10-19 17:31:56 -05:00
dependabot[bot] e95b90bc55 Build(deps): Bump @babel/traverse from 7.22.11 to 7.23.2 (#256)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.11 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 11:53:55 +02:00
Sam f65e50bd9e FIX: allow for blank fields in Google results (#255)
Under certain cases, for example:

```
there is this japanese band called kirimi, tell me more about them, try searching 3 times and at least 2 times in japanese before answering.
```

Results come back with blank snippets. This adds protection so this
is allowed and code does not simply blow up.
2023-10-19 14:44:59 +11:00
Rafael dos Santos Silva b9d6179bfc DEV: Migrations shouldn't rely on the app (#253) 2023-10-16 18:50:37 -03:00
David Taylor 2ed5a40d7d DEV: Fix .discourse-compatibility typo (#252)
Followup to e21405a600
2023-10-12 11:43:12 +01:00
Alan Guo Xiang Tan e21405a600 DEV: Fix compatibility file (#251)
Why this change?

We should be pinning to 3bced1c6f5 for
Discourse version smaller than `3.2.0.beta2-dev` which is the version
which the incompatibile change was introduced.
2023-10-12 13:50:07 +08:00
Roman Rizzi 38f383a1e0 FIX: Allowlist topic custom field used by AI Bot (#250) 2023-10-11 19:14:19 -03:00
Andrei Prigorshnev 4877394081 DEV: stop using the deprecated cookAsync function (#247)
The cookAsync functions is deprecated since discourse/discourse@fcc9d99.
2023-10-11 20:48:37 +04:00
Discourse Translator Bot 3bced1c6f5 Update translations (#249) 2023-10-11 11:18:14 +02:00
Roman Rizzi 919a87f8d7 FIX: Include OP when building title suggestion prompt. (#248)
When the OP is the only post in the topic, we'll send a prompt without user content to the LLM, and the suggested title will make no sense.
2023-10-10 14:08:08 +11:00
Keegan George 99ee949b33 DEV: Add revert button in diff modal (#246) 2023-10-06 15:17:21 -07:00
Sam 9242da545e FEATURE: support OpenAI-Organization header (#245)
Per: https://platform.openai.com/docs/api-reference/authentication

There is an organization option which is useful for large orgs

> For users who belong to multiple organizations, you can pass a header to specify which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota.
2023-10-06 10:23:18 +11:00
Sam d87adcebea FEATURE: Claude based scanning and OpenAI retries (#243)
llm_triage supported claude 2 in triage, this implements it

OpenAI rate limits frequently, this introduces some exponential
backoff (3 attempts - 3 seconds, 9 and 27)

Also reduces temp of classifiers so they have consistent behavior
2023-10-05 09:00:45 +11:00
Rafael dos Santos Silva 84cc369552 FEATURE: Bge-large-en embeddings via Cloudflare Workers AI API (#241)
* FEATURE: Bge-large-en embeddings via Cloudflare Workers AI API

* forgot a file

* lint
2023-10-04 13:47:51 -03:00
Discourse Translator Bot 05c256f65b Update translations (#239) 2023-10-04 09:54:32 +02:00
161 changed files with 33594 additions and 528 deletions
+1
View File
@@ -1,3 +1,4 @@
< 3.2.0.beta2-dev: 3bced1c6f525553fe2b5b79d29bd66c1cf2a47d5
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
< 3.2.0.beta1-dev: 3e7c99de894b03e9161d9481da118fe893991a1d
3.1.0.beta3: 057fbe1ce6
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "eslint-config-discourse"
}
+1
View File
@@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/eslint");
-1
View File
@@ -1 +0,0 @@
{}
+1
View File
@@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/prettier");
+2
View File
@@ -1,2 +1,4 @@
inherit_gem:
rubocop-discourse: stree-compat.yml
RSpec/NamedSubject:
Enabled: false
+1 -1
View File
@@ -1,2 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma,disable_ternary
--plugins=plugin/trailing_comma,plugin/disable_auto_ternary
+1 -4
View File
@@ -1,4 +1 @@
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};
module.exports = require("@discourse/lint-configs/template-lint");
-1
View File
@@ -5,5 +5,4 @@ source "https://rubygems.org"
group :development do
gem "rubocop-discourse"
gem "syntax_tree"
gem "syntax_tree-disable_ternary"
end
+28 -20
View File
@@ -2,36 +2,45 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.6.2)
parallel (1.22.1)
parser (3.1.2.1)
json (2.6.3)
language_server-protocol (3.17.0.3)
parallel (1.23.0)
parser (3.2.2.4)
ast (~> 2.4.1)
prettier_print (1.2.0)
racc
prettier_print (1.2.1)
racc (1.7.2)
rainbow (3.1.1)
regexp_parser (2.6.0)
rexml (3.2.5)
rubocop (1.36.0)
regexp_parser (2.8.2)
rexml (3.2.6)
rubocop (1.57.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.1.2.1)
parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.20.1, < 2.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.21.0)
parser (>= 3.1.1.0)
rubocop-discourse (3.0)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.19.0)
rubocop (~> 1.41)
rubocop-discourse (3.4.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.13.2)
rubocop-factory_bot (2.24.0)
rubocop (~> 1.33)
ruby-progressbar (1.11.0)
syntax_tree (5.1.0)
rubocop-rspec (2.25.0)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
syntax_tree-disable_ternary (1.0.0)
unicode-display_width (2.3.0)
unicode-display_width (2.5.0)
PLATFORMS
arm64-darwin-20
@@ -44,7 +53,6 @@ PLATFORMS
DEPENDENCIES
rubocop-discourse
syntax_tree
syntax_tree-disable_ternary
BUNDLED WITH
2.3.10
2.4.21
@@ -96,12 +96,37 @@ module DiscourseAi
end
end
def explain
post_id = get_post_param!
text = get_text_param!
post = Post.find_by(id: post_id)
raise Discourse::InvalidParameters.new(:post_id) unless post
render json:
DiscourseAi::AiHelper::TopicHelper.new(
{ text: text },
current_user,
post: post,
).explain,
status: 200
rescue ::DiscourseAi::Inference::OpenAiCompletions::CompletionFailed,
::DiscourseAi::Inference::HuggingFaceTextGeneration::CompletionFailed,
::DiscourseAi::Inference::AnthropicCompletions::CompletionFailed => e
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
status: 502
end
private
def get_text_param!
params[:text].tap { |t| raise Discourse::InvalidParameters.new(:text) if t.blank? }
end
def get_post_param!
params[:post_id].tap { |t| raise Discourse::InvalidParameters.new(:post_id) if t.blank? }
end
def rate_limiter_performed!
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
end
+2
View File
@@ -15,6 +15,8 @@ end
# id :bigint not null, primary key
# provider_id :integer not null
# user_id :integer
# topic_id :integer
# post_id :integer
# request_tokens :integer
# response_tokens :integer
# raw_request_payload :string
+2
View File
@@ -10,6 +10,8 @@ class CompletionPrompt < ActiveRecord::Base
validate :each_message_length
def messages_with_user_input(user_input)
return messages unless user_input.present?
if user_input[:custom_prompt].present?
case ::DiscourseAi::AiHelper::LlmPrompt.new.enabled_provider
when "huggingface"
@@ -1,9 +1,8 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
import I18n from "I18n";
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
export default class AiBotHeaderPanel extends Component {
@service siteSettings;
@@ -1,47 +1,15 @@
import Component from '@glimmer/component';
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import 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;
@@ -64,13 +32,33 @@ export default class AISuggestionDropdown extends Component {
get showAIButton() {
const minCharacterCount = 40;
return this.composer.model.replyLength > minCharacterCount;
const isShowAIButton = this.composer.model.replyLength > minCharacterCount;
const composerFields = document.querySelector(".composer-fields");
if (composerFields) {
if (isShowAIButton) {
composerFields.classList.add("showing-ai-suggestions");
} else {
composerFields.classList.remove("showing-ai-suggestions");
}
}
return isShowAIButton;
}
get disableSuggestionButton() {
return this.loading;
}
@action
applyClasses() {
if (this.showAIButton) {
document.querySelector(".composer-fields")?.classList.add("showing-ai-suggestions");
} else {
document.querySelector(".composer-fields")?.classList.remove("showing-ai-suggestions");
}
}
@bind
onClickOutside(event) {
const menu = document.querySelector(".ai-title-suggestions-menu");
@@ -98,16 +86,17 @@ export default class AISuggestionDropdown extends Component {
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();
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) {
@@ -122,7 +111,9 @@ export default class AISuggestionDropdown extends Component {
}
if (this.composer.model.replyLength === 0) {
return this.dialog.alert(I18n.t("discourse_ai.ai_helper.missing_content"));
return this.dialog.alert(
I18n.t("discourse_ai.ai_helper.missing_content")
);
}
this.loading = true;
@@ -131,16 +122,27 @@ export default class AISuggestionDropdown extends Component {
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;
});
})
.then((data) => {
this.#assignGeneratedSuggestions(data, this.args.mode);
})
.catch(popupAjaxError)
.finally(() => {
this.loading = false;
this.suggestIcon = "sync-alt";
this.showMenu = true;
if (this.args.mode === "suggest_category") {
document.querySelector(".category-input")?.classList.add("showing-ai-suggestion-menu");
}
});
}
#closeMenu() {
if (this.showMenu && this.args.mode === "suggest_category") {
document.querySelector(".category-input")?.classList.remove("showing-ai-suggestion-menu");
}
this.suggestIcon = "discourse-sparkles";
this.showMenu = false;
this.showErrors = false;
@@ -153,7 +155,9 @@ export default class AISuggestionDropdown extends Component {
if (!composer.tags) {
composer.set("tags", [suggestion]);
// remove tag from the list of suggestions once added
this.generatedSuggestions = this.generatedSuggestions.filter((s) => s !== suggestion);
this.generatedSuggestions = this.generatedSuggestions.filter(
(s) => s !== suggestion
);
return;
}
const tags = composer.tags;
@@ -161,14 +165,16 @@ export default class AISuggestionDropdown extends Component {
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});
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);
return (this.generatedSuggestions = this.generatedSuggestions.filter(
(s) => s !== suggestion
));
}
#tagSelectorHasValues() {
@@ -177,7 +183,7 @@ export default class AISuggestionDropdown extends Component {
#assignGeneratedSuggestions(data, mode) {
if (mode === this.SUGGESTION_TYPES.title) {
return this.generatedSuggestions = data.suggestions;
return (this.generatedSuggestions = data.suggestions);
}
const suggestions = data.assistant.map((s) => s.name);
@@ -185,12 +191,49 @@ export default class AISuggestionDropdown extends Component {
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));
return (this.generatedSuggestions = suggestions.filter(
(t) => !this.args.composer.tags.includes(t)
));
} else {
return this.generatedSuggestions = suggestions;
return (this.generatedSuggestions = suggestions);
}
}
return this.generatedSuggestions = suggestions;
return (this.generatedSuggestions = suggestions);
}
}
<template>
{{#if this.showAIButton}}
<DButton
@class="suggestion-button {{if this.loading 'is-loading'}}"
@icon={{this.suggestIcon}}
@title="discourse_ai.ai_helper.suggest"
@action={{this.performSuggestion}}
@disabled={{this.disableSuggestionButton}}
...attributes
/>
{{/if}}
{{#if this.showMenu}}
{{! template-lint-disable modifier-name-case }}
<ul
class="popup-menu ai-suggestions-menu"
{{didInsert this.handleClickOutside}}
>
{{#if this.showErrors}}
<li class="ai-suggestions-menu__errors">{{this.error}}</li>
{{/if}}
{{#each this.generatedSuggestions as |suggestion index|}}
<li data-name={{suggestion}} data-value={{index}}>
<DButton
@class="popup-menu-btn"
@translatedLabel={{suggestion}}
@action={{this.applySuggestion}}
@actionParam={{suggestion}}
/>
</li>
{{/each}}
</ul>
{{/if}}
</template>
}
@@ -1,14 +1,25 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import DModalCancel from "discourse/components/d-modal-cancel";
import I18n from "I18n";
import { htmlSafe } from "@ember/template";
const t = I18n.t.bind(I18n);
export default class ModalDiffModal extends Component {
@action
triggerConfirmChanges() {
this.args.closeModal();
this.args.confirm();
}
@action
triggerRevertChanges() {
this.args.closeModal();
this.args.revert();
}
<template>
<DModal
class="composer-ai-helper-modal"
@@ -35,14 +46,12 @@ export default class ModalDiffModal extends Component {
@action={{this.triggerConfirmChanges}}
@label="discourse_ai.ai_helper.context_menu.confirm"
/>
<DModalCancel @close={{@closeModal}} />
<DButton
class="btn-flat"
@action={{this.triggerRevertChanges}}
@label="discourse_ai.ai_helper.context_menu.revert"
/>
</:footer>
</DModal>
</template>
@action
triggerConfirmChanges() {
this.args.closeModal();
this.args.confirm();
}
}
@@ -1,19 +1,22 @@
import Component from '@glimmer/component';
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import showAIHelper from "../../lib/show-ai-helper";
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import { showComposerAIHelper } 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);
return showComposerAIHelper(outletArgs, helper);
}
@service siteSettings;
}
<template>
{{#if this.siteSettings.ai_embeddings_enabled}}
<AISuggestionDropdown
@mode="suggest_category"
@composer={{@outletArgs.composer}}
class="suggest-category-button"
/>
{{/if}}
</template>
}
@@ -1,19 +1,22 @@
import Component from '@glimmer/component';
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import showAIHelper from "../../lib/show-ai-helper";
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import { showComposerAIHelper } 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);
return showComposerAIHelper(outletArgs, helper);
}
@service siteSettings;
}
<template>
{{#if this.siteSettings.ai_embeddings_enabled}}
<AISuggestionDropdown
@mode="suggest_tags"
@composer={{@outletArgs.composer}}
class="suggest-tags-button"
/>
{{/if}}
</template>
}
@@ -1,13 +1,17 @@
import Component from '@glimmer/component';
import Component from "@glimmer/component";
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
import showAIHelper from "../../lib/show-ai-helper";
import { showComposerAIHelper } 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);
return showComposerAIHelper(outletArgs, helper);
}
}
<template>
<AISuggestionDropdown
@mode="suggest_title"
@composer={{@outletArgs.composer}}
class="suggest-titles-button"
/>
</template>
}
@@ -23,7 +23,9 @@
>
<Input
@value={{this.customPromptValue}}
placeholder="Enter a custom prompt..."
placeholder={{i18n
"discourse_ai.ai_helper.context_menu.custom_prompt.placeholder"
}}
class="ai-custom-prompt__input"
@enter={{action (fn this.updateSelected option)}}
/>
@@ -123,6 +125,7 @@
@diff={{this.diff}}
@oldValue={{this.selectedText}}
@newValue={{this.newSelectedText}}
@revert={{this.undoAIAction}}
@closeModal={{fn (mut this.showDiffModal) false}}
/>
{{/if}}
@@ -1,18 +1,18 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { afterRender, bind, debounce } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { createPopper } from "@popperjs/core";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { createPopper } from "@popperjs/core";
import { caretPosition, getCaretPosition } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
import showAIHelper from "../../lib/show-ai-helper";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { afterRender, bind, debounce } from "discourse-common/utils/decorators";
import { showComposerAIHelper } from "../../lib/show-ai-helper";
export default class AiHelperContextMenu extends Component {
static shouldRender(outletArgs, helper) {
return showAIHelper(outletArgs, helper);
return showComposerAIHelper(outletArgs, helper);
}
@service currentUser;
@@ -79,7 +79,9 @@ export default class AiHelperContextMenu extends Component {
async loadPrompts() {
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
prompts = prompts.filter((p) => p.name !== "generate_titles");
prompts = prompts
.filter((p) => p.location.includes("composer"))
.filter((p) => p.name !== "generate_titles");
// Find the custom_prompt object and move it to the beginning of the array
const customPromptIndex = prompts.findIndex(
@@ -1,5 +1,5 @@
{{#if this.isAiBotChat}}
<DSection @bodyClass="ai-bot-chat" />
<DSection @bodyClass={{this.aiBotClasses}} />
{{#if this.renderChatWarning}}
<div class="ai-bot-chat-warning">{{i18n
"discourse_ai.ai_bot.pm_warning"
@@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { computed } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "discourse-i18n";
export default class extends Component {
@service currentUser;
@@ -14,6 +15,18 @@ export default class extends Component {
return this.siteSettings.ai_bot_enable_chat_warning;
}
@computed("composerModel.targetRecipients", "composerModel.title")
get aiBotClasses() {
if (
this.composerModel?.title ===
I18n.t("discourse_ai.ai_bot.default_pm_prefix")
) {
return "ai-bot-chat";
} else {
return "ai-bot-pm";
}
}
@computed("composerModel.targetRecipients")
get isAiBotChat() {
if (
@@ -1,13 +1,13 @@
import Component from "@glimmer/component";
import { action, computed } from "@ember/object";
import I18n from "I18n";
import { tracked } from "@glimmer/tracking";
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import { SEARCH_TYPE_DEFAULT } from "discourse/controllers/full-page-search";
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";
import I18n from "I18n";
export default class extends Component {
static shouldRender(_args, { siteSettings }) {
@@ -0,0 +1,153 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "I18n";
import eq from "truth-helpers/helpers/eq";
import { showPostAIHelper } from "../../lib/show-ai-helper";
const i18n = I18n.t.bind(I18n);
export default class AIHelperOptionsMenu extends Component {
static shouldRender(outletArgs, helper) {
return showPostAIHelper(outletArgs, helper);
}
@tracked helperOptions = [];
@tracked menuState = this.MENU_STATES.triggers;
@tracked loading = false;
@tracked suggestion = "";
@tracked showMainButtons = true;
MENU_STATES = {
triggers: "TRIGGERS",
options: "OPTIONS",
loading: "LOADING",
result: "RESULT",
};
@tracked _activeAIRequest = null;
constructor() {
super(...arguments);
if (this.helperOptions.length === 0) {
this.loadPrompts();
}
}
@action
async showAIHelperOptions() {
this.showMainButtons = false;
this.menuState = this.MENU_STATES.options;
}
@action
async performAISuggestion(option) {
this.menuState = this.MENU_STATES.loading;
if (option.name === "Explain") {
this._activeAIRequest = ajax("/discourse-ai/ai-helper/explain", {
method: "POST",
data: {
mode: option.value,
text: this.args.outletArgs.data.quoteState.buffer,
post_id: this.args.outletArgs.data.quoteState.postId,
},
});
} else {
this._activeAIRequest = ajax("/discourse-ai/ai-helper/suggest", {
method: "POST",
data: {
mode: option.value,
text: this.args.outletArgs.data.quoteState.buffer,
custom_prompt: "",
},
});
}
this._activeAIRequest
.then(({ suggestions }) => {
this.suggestion = suggestions[0];
})
.catch(popupAjaxError)
.finally(() => {
this.loading = false;
this.menuState = this.MENU_STATES.result;
});
return this._activeAIRequest;
}
@action
cancelAIAction() {
if (this._activeAIRequest) {
this._activeAIRequest.abort();
this._activeAIRequest = null;
this.loading = false;
this.menuState = this.MENU_STATES.options;
}
}
async loadPrompts() {
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
this.helperOptions = prompts
.filter((item) => item.location.includes("post"))
.map((p) => {
return {
name: p.translated_name,
value: p.id,
icon: p.icon,
};
});
}
<template>
{{#if this.showMainButtons}}
{{yield}}
{{/if}}
<div class="ai-post-helper">
{{#if (eq this.menuState this.MENU_STATES.triggers)}}
<DButton
@class="btn-flat ai-post-helper__trigger"
@icon="discourse-sparkles"
@label="discourse_ai.ai_helper.post_options_menu.trigger"
@action={{this.showAIHelperOptions}}
/>
{{else if (eq this.menuState this.MENU_STATES.options)}}
<div class="ai-post-helper__options">
{{#each this.helperOptions as |option|}}
<DButton
@class="btn-flat"
@icon={{option.icon}}
@translatedLabel={{option.name}}
@action={{this.performAISuggestion}}
@actionParam={{option}}
data-name={{option.name}}
data-value={{option.value}}
/>
{{/each}}
</div>
{{else if (eq this.menuState this.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.MENU_STATES.result)}}
<div class="ai-post-helper__suggestion">{{this.suggestion}}</div>
{{/if}}
</div>
</template>
}
@@ -1,6 +1,6 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "I18n";
export default class extends Component {
@@ -30,7 +30,7 @@ export default class extends Component {
this.moreTopicsPreferenceTracking.registerTopicList({
name: I18n.t("discourse_ai.related_topics.pill"),
id: this.listId,
icon: "magic",
icon: "discourse-sparkles",
});
}
@@ -1,20 +1,38 @@
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)
export function showComposerAIHelper(outletArgs, helper) {
const enableHelper = _helperEnabled(helper.siteSettings);
const enableAssistant = _canUseAssistant(
helper.currentUser,
_findAllowedGroups(helper.siteSettings.ai_helper_allowed_groups)
);
const canShowInPM = helper.siteSettings.ai_helper_allowed_in_pm;
if (outletArgs?.composer?.privateMessage) {
return helperEnabled && canUseAssistant && canShowInPM;
return enableHelper && enableAssistant && canShowInPM;
}
return helperEnabled && canUseAssistant;
return enableHelper && enableAssistant;
}
export function showPostAIHelper(outletArgs, helper) {
return (
_helperEnabled(helper.siteSettings) &&
_canUseAssistant(
helper.currentUser,
_findAllowedGroups(helper.siteSettings.post_ai_helper_allowed_groups)
)
);
}
function _helperEnabled(siteSettings) {
return (
siteSettings.discourse_ai_enabled && siteSettings.composer_ai_helper_enabled
);
}
function _findAllowedGroups(setting) {
return setting.split("|").map((id) => parseInt(id, 10));
}
function _canUseAssistant(user, allowedGroups) {
return user?.groups.some((g) => allowedGroups.includes(g.id));
}
@@ -1,7 +1,7 @@
import Widget from "discourse/widgets/widget";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { hbs } from "ember-cli-htmlbars";
import { action } from "@ember/object";
import { hbs } from "ember-cli-htmlbars";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import Widget from "discourse/widgets/widget";
export default class AiBotHeaderPanelWrapper extends Widget {
buildAttributes() {
@@ -1,11 +1,11 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import { cookAsync } from "discourse/lib/text";
import { hbs } from "ember-cli-htmlbars";
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 { withPluginApi } from "discourse/lib/plugin-api";
import { cook } from "discourse/lib/text";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import { hbs } from "ember-cli-htmlbars";
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
function isGPTBot(user) {
return user && [-110, -111, -112].includes(user.id);
@@ -92,7 +92,7 @@ function initializeAIBotReplies(api) {
if (post) {
if (data.raw) {
cookAsync(data.raw).then((cooked) => {
cook(data.raw).then((cooked) => {
post.set("raw", data.raw);
post.set("cooked", cooked);
@@ -15,6 +15,18 @@ nav.post-controls .actions button.cancel-streaming {
}
}
.ai-bot-pm {
.gpt-persona {
margin-bottom: 5px;
}
#reply-control .composer-fields {
.mini-tag-chooser,
.add-warning {
display: none;
}
}
}
.ai-bot-chat-warning {
color: var(--tertiary);
background-color: var(--tertiary-low);

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