Compare commits

..
Author SHA1 Message Date
Joffrey JAFFEUX 91a97d75da FIX: prevents double escaping of filters
We were passing: `{"category": 4}` which was then escape by Ember, basically causing a double Json stringify and preventing code to work down the road.
2025-03-10 21:05:38 +01:00
263 changed files with 3978 additions and 6046 deletions
-1
View File
@@ -1,4 +1,3 @@
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
< 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
+45 -59
View File
@@ -1,93 +1,79 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (8.0.2)
activesupport (7.1.3.4)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
mutex_m
tzinfo (~> 2.0)
ast (2.4.2)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
bigdecimal (3.1.8)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
drb (2.2.1)
i18n (1.14.7)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
json (2.10.2)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
logger (1.6.6)
minitest (5.25.5)
parallel (1.26.3)
parser (3.3.7.1)
json (2.7.2)
language_server-protocol (3.17.0.3)
minitest (5.24.1)
mutex_m (0.2.0)
parallel (1.25.1)
parser (3.3.3.0)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
racc (1.8.1)
rack (3.1.12)
racc (1.8.0)
rack (3.1.10)
rainbow (3.1.1)
regexp_parser (2.10.0)
rubocop (1.74.0)
regexp_parser (2.9.2)
rexml (3.3.9)
rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.38.0, < 2.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.1)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
parser (>= 3.3.1.0)
rubocop-capybara (2.22.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-discourse (3.12.1)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-discourse (3.8.1)
activesupport (>= 6.1)
lint_roller (>= 1.1.0)
rubocop (>= 1.73.2)
rubocop-capybara (>= 2.22.0)
rubocop-factory_bot (>= 2.27.0)
rubocop-rails (>= 2.30.3)
rubocop (>= 1.59.0)
rubocop-capybara (>= 2.0.0)
rubocop-factory_bot (>= 2.0.0)
rubocop-rails (>= 2.25.0)
rubocop-rspec (>= 3.0.1)
rubocop-rspec_rails (>= 2.31.0)
rubocop-factory_bot (2.27.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.30.3)
rubocop-rspec_rails (>= 2.30.0)
rubocop-factory_bot (2.26.1)
rubocop (~> 1.61)
rubocop-rails (2.25.1)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rspec (3.5.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.2)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
unicode-display_width (2.5.0)
PLATFORMS
ruby
@@ -97,4 +83,4 @@ DEPENDENCIES
syntax_tree
BUNDLED WITH
2.6.6
2.5.10
+1 -2
View File
@@ -1,8 +1,7 @@
{
"tests": {
"requiredPlugins": [
"https://github.com/discourse/discourse-prometheus",
"https://github.com/discourse/discourse-solved.git"
"https://github.com/discourse/discourse-prometheus"
]
}
}
@@ -15,7 +15,7 @@ module DiscourseAi
LocalizedAiPersonaSerializer.new(persona, root: false)
end
tools =
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
AiToolSerializer.new(tool, root: false)
end
AiTool
@@ -53,12 +53,7 @@ module DiscourseAi
end
def suggest_title
if params[:topic_id]
topic = Topic.find_by(id: params[:topic_id])
input = DiscourseAi::Summarization::Strategies::TopicSummary.new(topic).targets_data
else
input = get_text_param!
end
input = get_text_param!
prompt = CompletionPrompt.enabled_by_name("generate_titles")
raise Discourse::InvalidParameters.new(:mode) if !prompt
@@ -78,26 +73,22 @@ module DiscourseAi
end
def suggest_category
if params[:topic_id]
opts = { topic_id: params[:topic_id] }
else
input = get_text_param!
opts = { text: input }
end
input = get_text_param!
input_hash = { text: input }
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(current_user, opts).categories,
render json:
DiscourseAi::AiHelper::SemanticCategorizer.new(
input_hash,
current_user,
).categories,
status: 200
end
def suggest_tags
if params[:topic_id]
opts = { topic_id: params[:topic_id] }
else
input = get_text_param!
opts = { text: input }
end
input = get_text_param!
input_hash = { text: input }
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(current_user, opts).tags,
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(input_hash, current_user).tags,
status: 200
end
+2 -2
View File
@@ -12,11 +12,11 @@ module ::Jobs
return if message.blank?
personaClass =
DiscourseAi::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
return if personaClass.blank?
user = User.find_by(id: personaClass.user_id)
bot = DiscourseAi::Personas::Bot.as(user, persona: personaClass.new)
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
message,
+4 -4
View File
@@ -10,13 +10,13 @@ module ::Jobs
persona_id = args[:persona_id]
begin
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
rescue DiscourseAi::AiBot::Bot::BOT_NOT_FOUND
Rails.logger.warn(
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
)
+1 -1
View File
@@ -19,7 +19,7 @@ module Jobs
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
bot =
DiscourseAi::Personas::Bot.as(
DiscourseAi::AiBot::Bot.as(
Discourse.system_user,
persona: ai_persona_klass.new,
model: llm_model,
+4 -4
View File
@@ -201,14 +201,14 @@ class AiPersona < ActiveRecord::Base
if inner_name.start_with?("custom-")
custom_tool_id = inner_name.split("-", 2).last.to_i
if AiTool.exists?(id: custom_tool_id, enabled: true)
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id)
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
end
else
inner_name = inner_name.gsub("Tool", "")
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
begin
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize
klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize
options[klass] = current_options if current_options
rescue StandardError
end
@@ -218,7 +218,7 @@ class AiPersona < ActiveRecord::Base
klass
end
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
if persona_class
instance_attributes.each do |key, value|
# description/name are localized
@@ -230,7 +230,7 @@ class AiPersona < ActiveRecord::Base
ai_persona_id = self.id
Class.new(DiscourseAi::Personas::Persona) do
Class.new(DiscourseAi::AiBot::Personas::Persona) do
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
define_singleton_method(:to_s) do
-8
View File
@@ -26,23 +26,17 @@ class LlmModel < ActiveRecord::Base
access_key_id: :text,
region: :text,
disable_native_tools: :checkbox,
disable_temperature: :checkbox,
disable_top_p: :checkbox,
enable_reasoning: :checkbox,
reasoning_tokens: :number,
},
anthropic: {
disable_native_tools: :checkbox,
disable_temperature: :checkbox,
disable_top_p: :checkbox,
enable_reasoning: :checkbox,
reasoning_tokens: :number,
},
open_ai: {
organization: :text,
disable_native_tools: :checkbox,
disable_temperature: :checkbox,
disable_top_p: :checkbox,
disable_streaming: :checkbox,
reasoning_effort: {
type: :enum,
@@ -75,8 +69,6 @@ class LlmModel < ActiveRecord::Base
provider_order: :text,
provider_quantizations: :text,
disable_streaming: :checkbox,
disable_temperature: :checkbox,
disable_top_p: :checkbox,
},
}
end
@@ -1,3 +1,4 @@
import { tracked } from "@glimmer/tracking";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
@@ -62,7 +63,40 @@ const SYSTEM_ATTRIBUTES = [
"allow_chat_direct_messages",
];
class ToolOption {
@tracked value = null;
}
export default class AiPersona extends RestModel {
// this code is here to convert the wire schema to easier to work with object
// on the wire we pass in/out tools as an Array.
// [[ToolName, {option1: value, option2: value}, force], ToolName2, ToolName3]
// So we rework this into a "tools" property and nested toolOptions
init(properties) {
this.forcedTools = [];
if (properties.tools) {
properties.tools = properties.tools.map((tool) => {
if (typeof tool === "string") {
return tool;
} else {
let [toolId, options, force] = tool;
for (let optionId in options) {
if (!options.hasOwnProperty(optionId)) {
continue;
}
this.getToolOption(toolId, optionId).value = options[optionId];
}
if (force) {
this.forcedTools.push(toolId);
}
return toolId;
}
});
}
super.init(properties);
this.tools = properties.tools;
}
async createUser() {
const result = await ajax(
`/admin/plugins/discourse-ai/ai-personas/${this.id}/create-user.json`,
@@ -75,79 +109,63 @@ export default class AiPersona extends RestModel {
return this.user;
}
flattenedToolStructure(data) {
return (data.tools || []).map((tName) => {
return [tName, data.toolOptions[tName], data.forcedTools.includes(tName)];
});
getToolOption(toolId, optionId) {
this.toolOptions ||= {};
this.toolOptions[toolId] ||= {};
return (this.toolOptions[toolId][optionId] ||= new ToolOption());
}
// this code is here to convert the wire schema to easier to work with object
// on the wire we pass in/out tools as an Array.
// [[ToolName, {option1: value, option2: value}, force], ToolName2, ToolName3]
// We split it into tools, options and a list of forced ones.
populateTools(attrs) {
const forcedTools = [];
const toolOptions = {};
populateToolOptions(attrs) {
if (!attrs.tools) {
return;
}
let toolsWithOptions = [];
attrs.tools.forEach((toolId) => {
if (typeof toolId !== "string") {
toolId = toolId[0];
}
const flatTools = attrs.tools?.map((tool) => {
if (typeof tool === "string") {
return tool;
} else {
let [toolId, options, force] = tool;
const mappedOptions = {};
for (const optionId in options) {
let force = this.forcedTools.includes(toolId);
if (this.toolOptions && this.toolOptions[toolId]) {
let options = this.toolOptions[toolId];
let optionsWithValues = {};
for (let optionId in options) {
if (!options.hasOwnProperty(optionId)) {
continue;
}
mappedOptions[optionId] = options[optionId];
let option = options[optionId];
optionsWithValues[optionId] = option.value;
}
if (Object.keys(mappedOptions).length > 0) {
toolOptions[toolId] = mappedOptions;
}
if (force) {
forcedTools.push(toolId);
}
return toolId;
toolsWithOptions.push([toolId, optionsWithValues, force]);
} else {
toolsWithOptions.push([toolId, {}, force]);
}
});
attrs.tools = flatTools;
attrs.forcedTools = forcedTools;
attrs.toolOptions = toolOptions;
attrs.tools = toolsWithOptions;
}
updateProperties() {
const attrs = this.system
let attrs = this.system
? this.getProperties(SYSTEM_ATTRIBUTES)
: this.getProperties(CREATE_ATTRIBUTES);
attrs.id = this.id;
this.populateToolOptions(attrs);
return attrs;
}
createProperties() {
return this.getProperties(CREATE_ATTRIBUTES);
}
fromPOJO(data) {
const dataClone = JSON.parse(JSON.stringify(data));
const persona = AiPersona.create(dataClone);
persona.tools = this.flattenedToolStructure(dataClone);
return persona;
}
toPOJO() {
const attrs = this.getProperties(CREATE_ATTRIBUTES);
this.populateTools(attrs);
attrs.forced_tool_count = this.forced_tool_count || -1;
let attrs = this.getProperties(CREATE_ATTRIBUTES);
this.populateToolOptions(attrs);
return attrs;
}
workingCopy() {
let attrs = this.getProperties(CREATE_ATTRIBUTES);
this.populateToolOptions(attrs);
const persona = AiPersona.create(attrs);
persona.forcedTools = (this.forcedTools || []).slice();
persona.forced_tool_count = this.forced_tool_count || -1;
return persona;
}
}
@@ -23,7 +23,6 @@ import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
import DoughnutChart from "discourse/plugins/discourse-ai/discourse/components/doughnut-chart";
import AiSentimentHorizontalBar from "../components/ai-sentiment-horizontal-bar";
export default class AdminReportSentimentAnalysis extends Component {
@service router;
@@ -83,15 +82,6 @@ export default class AdminReportSentimentAnalysis extends Component {
}
}
get groupingType() {
const dataSample = this.args.model.data[0];
const localePrefix =
"discourse_ai.sentiments.sentiment_analysis.group_types";
return dataSample.category_name
? i18n(`${localePrefix}.category`)
: i18n(`${localePrefix}.tag`);
}
get colors() {
return ["#2ecc71", "#95a5a6", "#e74c3c"];
}
@@ -117,17 +107,7 @@ export default class AdminReportSentimentAnalysis extends Component {
this.calculateNeutralScore(data),
data.negative_count,
],
score_map: {
positive: data.positive_count,
neutral: this.calculateNeutralScore(data),
negative: data.negative_count,
},
total_score: data.total_count,
widths: {
positive: (data.positive_count / data.total_count) * 100,
neutral: (this.calculateNeutralScore(data) / data.total_count) * 100,
negative: (data.negative_count / data.total_count) * 100,
},
};
});
}
@@ -236,15 +216,10 @@ export default class AdminReportSentimentAnalysis extends Component {
}
const currentQueryParams = this.router.currentRoute.queryParams;
const currentFilters = currentQueryParams?.filters
? JSON.parse(currentQueryParams.filters)
: {};
this.router.transitionTo(this.router.currentRoute.name, {
queryParams: {
...currentQueryParams,
filters: currentFilters, // avoids a double escaping
filters: JSON.parse(currentQueryParams.filters), // avoids a double escaping
selectedChart: data.title,
},
});
@@ -290,13 +265,9 @@ export default class AdminReportSentimentAnalysis extends Component {
this.posts = [];
const currentQueryParams = this.router.currentRoute.queryParams;
const currentFilters = currentQueryParams?.filters
? JSON.parse(currentQueryParams.filters)
: {};
this.router.transitionTo(this.router.currentRoute.name, {
queryParams: {
...currentQueryParams,
filters: currentFilters, // avoids a double escaping
selectedChart: null,
},
});
@@ -322,56 +293,29 @@ export default class AdminReportSentimentAnalysis extends Component {
{{#unless this.showingSelectedChart}}
<div class="admin-report-sentiment-analysis">
<table class="sentiment-analysis-table md-table">
<thead>
<th>{{this.groupingType}}</th>
<th>{{i18n
"discourse_ai.sentiments.sentiment_analysis.table.total_count"
}}</th>
<th>{{i18n
"discourse_ai.sentiments.sentiment_analysis.table.sentiment"
}}</th>
</thead>
<tbody>
{{#each this.transformedData as |data|}}
<tr
class="sentiment-analysis-table__row"
role="button"
{{on "click" (fn this.showDetails data)}}
{{closeOnClickOutside
(fn (mut this.selectedChart) null)
(hash
targetSelector=".admin-report-sentiment-analysis-details"
secondaryTargetSelector=".admin-report-sentiment-analysis"
)
}}
>
<td class="sentiment-analysis-table__title">{{data.title}}</td>
<td
class="sentiment-analysis-table__total-score"
>{{data.total_score}}</td>
<td class="sentiment-horizontal-bar">
<AiSentimentHorizontalBar
@type="positive"
@score={{data.score_map.positive}}
@width={{data.widths.positive}}
/>
<AiSentimentHorizontalBar
@type="negative"
@score={{data.score_map.negative}}
@width={{data.widths.negative}}
/>
<AiSentimentHorizontalBar
@type="neutral"
@score={{data.score_map.neutral}}
@width={{data.widths.neutral}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{#each this.transformedData as |data|}}
<div
class="admin-report-sentiment-analysis__chart-wrapper"
role="button"
{{on "click" (fn this.showDetails data)}}
{{closeOnClickOutside
(fn (mut this.selectedChart) null)
(hash
targetSelector=".admin-report-sentiment-analysis-details"
secondaryTargetSelector=".admin-report-sentiment-analysis"
)
}}
>
<DoughnutChart
@labels={{@model.labels}}
@colors={{this.colors}}
@data={{data.scores}}
@totalScore={{data.total_score}}
@doughnutTitle={{data.title}}
@displayLegend={{true}}
/>
</div>
{{/each}}
</div>
{{/unless}}
@@ -13,7 +13,6 @@ import getURL from "discourse/lib/get-url";
export default class AiArtifactComponent extends Component {
@service siteSettings;
@tracked expanded = false;
@tracked showingArtifact = false;
@@ -15,7 +15,6 @@ export default class AiComposerHelperMenu extends Component {
@service siteSettings;
@service currentUser;
@service site;
@tracked newSelectedText;
@tracked diff;
@tracked customPromptValue = "";
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
import { computed } from "@ember/object";
import { i18n } from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
export default ComboBox.extend({
content: computed(function () {
const content = [
{
id: -1,
name: i18n("discourse_ai.ai_persona.tool_strategies.all"),
},
];
[1, 2, 5].forEach((i) => {
content.push({
id: i,
name: i18n("discourse_ai.ai_persona.tool_strategies.replies", {
count: i,
}),
});
});
return content;
}),
selectKitOptions: {
filterable: false,
},
});
@@ -172,6 +172,9 @@ export default class AiFullPageSearch extends Component {
this.AiResults = [];
this.showingAiResults = false;
this.args.addSearchResults([], "topic_id");
this.appEvents.trigger(AI_RESULTS_TOGGLED, {
enabled: false,
});
}
performHyDESearch() {
@@ -250,11 +250,6 @@ export default class AiLlmEditorForm extends Component {
});
}
@action
providerParamsKeys(providerParams) {
return providerParams ? Object.keys(providerParams) : [];
}
<template>
<Form
@onSubmit={{this.save}}
@@ -339,36 +334,32 @@ export default class AiLlmEditorForm extends Component {
<field.Password />
</form.Field>
<form.Object @name="provider_params" as |object providerParamsData|>
{{#each (this.providerParamsKeys providerParamsData) as |name|}}
{{#let
(get (this.metaProviderParams data.provider) name)
as |params|
}}
<object.Field
@name={{name}}
@title={{i18n
(concat "discourse_ai.llms.provider_fields." name)
}}
@format="large"
as |field|
>
{{#if (eq params.type "enum")}}
<field.Select @includeNone={{false}} as |select|>
{{#each params.values as |option|}}
<select.Option
@value={{option.id}}
>{{option.name}}</select.Option>
{{/each}}
</field.Select>
{{else if (eq params.type "checkbox")}}
<field.Checkbox />
{{else}}
<field.Input @type={{params.type}} />
{{/if}}
</object.Field>
{{/let}}
{{/each}}
<form.Object @name="provider_params" as |object name|>
{{#let
(get (this.metaProviderParams data.provider) name)
as |params|
}}
<object.Field
@name={{name}}
@title={{i18n (concat "discourse_ai.llms.provider_fields." name)}}
@format="large"
as |field|
>
{{#if (eq params.type "enum")}}
<field.Select @includeNone={{false}} as |select|>
{{#each params.values as |option|}}
<select.Option
@value={{option.id}}
>{{option.name}}</select.Option>
{{/each}}
</field.Select>
{{else if (eq params.type "checkbox")}}
<field.Checkbox />
{{else}}
<field.Input @type={{params.type}} />
{{/if}}
</object.Field>
{{/let}}
</form.Object>
<form.Field
@@ -564,24 +555,32 @@ export default class AiLlmEditorForm extends Component {
{{/unless}}
{{#if this.displayTestResult}}
<form.Container @format="full">
<ConditionalLoadingSpinner
@size="small"
@condition={{this.testRunning}}
>
{{#if this.testResult}}
<div class="ai-llm-editor-tests__success">
{{icon "check"}}
{{i18n "discourse_ai.llms.tests.success"}}
</div>
{{else}}
<div class="ai-llm-editor-tests__failure">
{{icon "xmark"}}
{{this.testErrorMessage}}
</div>
{{/if}}
</ConditionalLoadingSpinner>
</form.Container>
<form.Field
@showTitle={{false}}
@name="test_results"
@title="test_results"
@format="full"
as |field|
>
<field.Custom>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.testRunning}}
>
{{#if this.testResult}}
<div class="ai-llm-editor-tests__success">
{{icon "check"}}
{{i18n "discourse_ai.llms.tests.success"}}
</div>
{{else}}
<div class="ai-llm-editor-tests__failure">
{{icon "xmark"}}
{{this.testErrorMessage}}
</div>
{{/if}}
</ConditionalLoadingSpinner>
</field.Custom>
</form.Field>
{{/if}}
</Form>
</template>
@@ -1,17 +0,0 @@
import { hash } from "@ember/helper";
import ComboBox from "select-kit/components/combo-box";
const AiLlmSelector = <template>
<ComboBox
@value={{@value}}
@content={{@llms}}
@onChange={{@onChange}}
@options={{hash
filterable=true
none="discourse_ai.ai_persona.no_llm_selected"
}}
class={{@class}}
/>
</template>;
export default AiLlmSelector;
@@ -0,0 +1,27 @@
import { computed } from "@ember/object";
import { observes } from "@ember-decorators/object";
import { i18n } from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
import { selectKitOptions } from "select-kit/components/select-kit";
@selectKitOptions({
filterable: true,
})
export default class AiLlmSelector extends ComboBox {
@observes("attrs.disabled")
_modelDisabledChanged() {
this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
}
@computed
get content() {
const blankName =
this.attrs.blankName || i18n("discourse_ai.ai_persona.no_llm_selected");
return [
{
id: "blank",
name: blankName,
},
].concat(this.llms);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,91 @@
import Component from "@glimmer/component";
import { Input } from "@ember/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { eq } from "truth-helpers";
import { i18n } from "discourse-i18n";
import AiLlmSelector from "./ai-llm-selector";
export default class AiPersonaToolOptionEditor extends Component {
get isBoolean() {
return this.args.option.type === "boolean";
}
get isEnum() {
return this.args.option.type === "enum";
}
get isLlm() {
return this.args.option.type === "llm";
}
get selectedValue() {
return this.args.option.value.value === "true";
}
get selectedLlm() {
if (this.args.option.value.value) {
return `custom:${this.args.option.value.value}`;
} else {
return "blank";
}
}
set selectedLlm(value) {
if (value === "blank") {
this.args.option.value.value = null;
} else {
this.args.option.value.value = value.replace("custom:", "");
}
}
@action
onCheckboxChange(event) {
this.args.option.value.value = event.target.checked ? "true" : "false";
}
@action
onSelectOption(event) {
this.args.option.value.value = event.target.value;
}
<template>
<div class="control-group ai-persona-tool-option-editor">
<label>
{{@option.name}}
</label>
<div class="">
{{#if this.isEnum}}
<select name="input" {{on "change" this.onSelectOption}}>
{{#each @option.values as |value|}}
<option value={{value}} selected={{eq value @option.value.value}}>
{{value}}
</option>
{{/each}}
</select>
{{else if this.isLlm}}
<AiLlmSelector
class="ai-persona-tool-option-editor__llms"
@value={{this.selectedLlm}}
@llms={{@llms}}
@blankName={{i18n "discourse_ai.ai_persona.use_parent_llm"}}
/>
{{else if this.isBoolean}}
<input
type="checkbox"
checked={{this.selectedValue}}
{{on "click" this.onCheckboxChange}}
/>
{{@option.description}}
{{else}}
<Input @value={{@option.value.value}} />
{{/if}}
</div>
{{#unless this.isBoolean}}
<div class="ai-persona-tool-option-editor__instructions">
{{@option.description}}
</div>
{{/unless}}
</div>
</template>
}
@@ -1,95 +1,82 @@
import Component from "@glimmer/component";
import { action, get } from "@ember/object";
import { eq } from "truth-helpers";
import { i18n } from "discourse-i18n";
import AiLlmSelector from "./ai-llm-selector";
import AiPersonaToolOptionEditor from "./ai-persona-tool-option-editor";
export default class AiPersonaToolOptions extends Component {
get showToolOptions() {
const allTools = this.args.allTools;
if (!allTools || !this.args.data.tools) {
if (!allTools) {
return false;
}
return this.args.data?.tools.any(
(tool) => allTools.findBy("id", tool)?.options
);
return this.toolNames.any((tool) => allTools.findBy("id", tool)?.options);
}
get toolsMetadata() {
const metatada = {};
get toolNames() {
if (!this.args.tools) {
return [];
}
return this.args.tools.map((tool) => {
if (typeof tool === "string") {
return tool;
} else {
return tool[0];
}
});
}
this.args.allTools.map((t) => {
metatada[t.id] = {
name: t.name,
...t?.options,
};
get toolOptions() {
if (!this.args.tools) {
return [];
}
const allTools = this.args.allTools;
if (!allTools) {
return [];
}
const options = [];
this.toolNames.forEach((toolId) => {
const tool = allTools.findBy("id", toolId);
const toolName = tool?.name;
const toolOptions = tool?.options;
if (toolOptions) {
const mappedOptions = Object.keys(toolOptions).map((key) => {
const value = this.args.persona.getToolOption(toolId, key);
return Object.assign({}, toolOptions[key], { id: key, value });
});
options.push({ toolName, options: mappedOptions });
}
});
return metatada;
}
@action
formObjectKeys(toolOptions) {
return toolOptions ? Object.keys(toolOptions) : [];
return options;
}
<template>
{{#if this.showToolOptions}}
<@form.Container
@title={{i18n "discourse_ai.ai_persona.tool_options"}}
@direction="column"
@format="full"
>
<@form.Object
@name="toolOptions"
@title={{i18n "discourse_ai.ai_persona.tool_options"}}
as |toolObj optsPerTool|
>
{{#each (this.formObjectKeys optsPerTool) as |toolId|}}
<div class="control-group">
<label>{{i18n "discourse_ai.ai_persona.tool_options"}}</label>
<div>
{{#each this.toolOptions as |toolOption|}}
<div class="ai-persona-editor__tool-options">
{{#let (get this.toolsMetadata toolId) as |toolMeta|}}
<div class="ai-persona-editor__tool-options-name">
{{toolMeta.name}}
</div>
<toolObj.Object @name={{toolId}} as |optionsObj optionData|>
{{#each (this.formObjectKeys optionData) as |optionName|}}
{{#let (get toolMeta optionName) as |optionMeta|}}
<optionsObj.Field
@name={{optionName}}
@title={{optionMeta.name}}
@helpText={{optionMeta.description}}
@format="full"
as |field|
>
{{#if (eq optionMeta.type "enum")}}
<field.Select @includeNone={{false}} as |select|>
{{#each optionsObj.values as |v|}}
<select.Option @value={{v}}>{{v}}</select.Option>
{{/each}}
</field.Select>
{{else if (eq optionMeta.type "llm")}}
<field.Custom>
<AiLlmSelector
@value={{field.value}}
@llms={{@llms}}
@onChange={{field.set}}
@class="ai-persona-tool-option-editor__llms"
/>
</field.Custom>
{{else if (eq optionMeta.type "boolean")}}
<field.Checkbox />
{{else}}
<field.Input />
{{/if}}
</optionsObj.Field>
{{/let}}
{{/each}}
</toolObj.Object>
{{/let}}
<div class="ai-persona-editor__tool-options-name">
{{toolOption.toolName}}
</div>
<div class="ai-persona-editor__tool-option-options">
{{#each toolOption.options as |option|}}
<AiPersonaToolOptionEditor
@option={{option}}
@llms={{@llms}}
/>
{{/each}}
</div>
</div>
{{/each}}
</@form.Object>
</@form.Container>
</div>
</div>
{{/if}}
</template>
}
@@ -1,49 +0,0 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
export default class AiSearchDiscoveriesTooltip extends Component {
@service discobotDiscoveries;
<template>
<span class="ai-search-discoveries-tooltip">
<DTooltip @placement="top-end" @interactive={{true}}>
<:trigger>
{{icon "circle-info"}}
</:trigger>
<:content>
<div class="ai-search-discoveries-tooltip__content">
<div class="ai-search-discoveries-tooltip__header">
{{i18n "discourse_ai.discobot_discoveries.tooltip.header"}}
</div>
<div class="ai-search-discoveries-tooltip__description">
{{#if this.discobotDiscoveries.modelUsed}}
{{i18n
"discourse_ai.discobot_discoveries.tooltip.content"
model=this.discobotDiscoveries.modelUsed
}}
{{/if}}
</div>
<div class="ai-search-discoveries-tooltip__actions">
<DButton
class="btn-transparent btn-primary"
@label="discourse_ai.discobot_discoveries.tooltip.actions.info"
@href="https://meta.discourse.org/t/conversational-ai-search-coming-to-discourse-ai/355939"
/>
<DButton
class="btn-transparent btn-danger"
@label="discourse_ai.discobot_discoveries.tooltip.actions.disable"
@action={{this.discobotDiscoveries.disableDiscoveries}}
/>
</div>
</div>
</:content>
</DTooltip>
</span>
</template>
}
@@ -1,30 +0,0 @@
import { concat } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import { gt } from "truth-helpers";
import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
const AiSentimentHorizontalBar = <template>
{{#if (gt @score 0)}}
<DTooltip
class={{concat "sentiment-horizontal-bar__" @type}}
style={{htmlSafe (concat "width: " @width "%")}}
>
<:trigger>
<span class="sentiment-horizontal-bar__count">
{{@score}}
</span>
</:trigger>
<:content>
{{i18n
(concat
"discourse_ai.sentiments.sentiment_analysis.filter_types." @type
)
}}:
{{@score}}
</:content>
</DTooltip>
{{/if}}
</template>;
export default AiSentimentHorizontalBar;
@@ -1,13 +0,0 @@
import { hash } from "@ember/helper";
import MultiSelect from "select-kit/components/multi-select";
const AiToolSelector = <template>
<MultiSelect
@value={{@value}}
@onChange={{@onChange}}
@content={{@content}}
@options={{hash filterable=true allowAny=false disabled=@disabled}}
/>
</template>;
export default AiToolSelector;
@@ -0,0 +1,18 @@
import { readOnly } from "@ember/object/computed";
import { observes } from "@ember-decorators/object";
import MultiSelectComponent from "select-kit/components/multi-select";
import { selectKitOptions } from "select-kit/components/select-kit";
@selectKitOptions({
filterable: true,
})
export default class AiToolSelector extends MultiSelectComponent {
@readOnly("tools") content;
value = "";
@observes("attrs.disabled")
_modelDisabledChanged() {
this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
}
}
@@ -23,7 +23,6 @@ import ComboBox from "select-kit/components/combo-box";
export default class AiUsage extends Component {
@service store;
@tracked startDate = moment().subtract(30, "days").toDate();
@tracked endDate = new Date();
@tracked data = this.args.model;
@@ -13,7 +13,6 @@ import AiIndicatorWave from "../ai-indicator-wave";
export default class ModalDiffModal extends Component {
@service currentUser;
@tracked loading = false;
@tracked diff;
@tracked suggestion = "";
@@ -13,7 +13,6 @@ import { i18n } from "discourse-i18n";
export default class ShareModal extends Component {
@service toasts;
@tracked shareKey = "";
constructor() {

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