Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f10af7b167 | ||
|
|
54ebbbe769 | ||
|
|
8b33ba3d19 | ||
|
|
f1c7ee8624 | ||
|
|
4da033c667 | ||
|
|
dabef02919 | ||
|
|
fbcc8e493a | ||
|
|
6b7d7c1179 | ||
|
|
0d3e6b2726 | ||
|
|
be05e28679 | ||
|
|
2f7895bb91 | ||
|
|
7a094cde18 | ||
|
|
251628bfa1 | ||
|
|
ef07fcb308 | ||
|
|
ddf2bf7034 | ||
|
|
23193ee6f2 | ||
|
|
54f2d34ccb | ||
|
|
616b990894 | ||
|
|
6c25718a7f | ||
|
|
79021252e9 | ||
|
|
aeba72441d | ||
|
|
95762723de | ||
|
|
5fb1177f7b | ||
|
|
ea24328415 | ||
|
|
f8231d259b | ||
|
|
e54f2da1a5 | ||
|
|
1921e4e90d | ||
|
|
2cc8115b48 | ||
|
|
86cf4ccba7 | ||
|
|
2c78961bed | ||
|
|
8e00e036e1 | ||
|
|
d56ed53eb1 | ||
|
|
d83248cf68 | ||
|
|
872e43e849 | ||
|
|
2900d2c87d | ||
|
|
b68a4ca718 | ||
|
|
1a10680818 | ||
|
|
52c644798d | ||
|
|
2652716398 | ||
|
|
a0aec48606 | ||
|
|
f09e74c05e | ||
|
|
2c8d81827f | ||
|
|
a9afa04329 | ||
|
|
530a795d43 | ||
|
|
dcde94a393 | ||
|
|
3c91f374ac | ||
|
|
fb80d776d8 | ||
|
|
48d08dedd4 | ||
|
|
b10be23533 | ||
|
|
6b9c66054c | ||
|
|
3ae1e4eaf0 | ||
|
|
755b63f31f | ||
|
|
0d7f353284 | ||
|
|
4fb686a548 | ||
|
|
5026ab52d0 | ||
|
|
823e8ef490 | ||
|
|
f75b13c4fa | ||
|
|
9551b1a4d1 | ||
|
|
aef9a03d4c | ||
|
|
9583964676 | ||
|
|
2fc05685bb | ||
|
|
26bfda4763 | ||
|
|
6ef635619f | ||
|
|
e817b7dc11 | ||
|
|
644141ff08 | ||
|
|
fbc74c7467 | ||
|
|
c421f713a3 | ||
|
|
021e09607d | ||
|
|
893fa624e4 | ||
|
|
1ad5321c09 | ||
|
|
da7c97d294 | ||
|
|
ab4678275d | ||
|
|
99282612a9 |
@@ -1 +0,0 @@
|
||||
module.exports = require("@discourse/lint-configs/eslint");
|
||||
+7
-5
@@ -1,5 +1,7 @@
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
<section class="ai-persona-tool-editor__current admin-detail pull-left">
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
</section>
|
||||
+7
-5
@@ -1,5 +1,7 @@
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
<section class="ai-persona-tool-editor__current admin-detail pull-left">
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
</section>
|
||||
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
class ArtifactsController < ApplicationController
|
||||
requires_plugin DiscourseAi::PLUGIN_NAME
|
||||
before_action :require_site_settings!
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, only: %i[show]
|
||||
|
||||
def show
|
||||
artifact = AiArtifact.find(params[:id])
|
||||
|
||||
post = Post.find_by(id: artifact.post_id)
|
||||
if artifact.metadata&.dig("public")
|
||||
# no guardian needed
|
||||
else
|
||||
raise Discourse::NotFound if !post&.topic&.private_message?
|
||||
raise Discourse::NotFound if !guardian.can_see?(post)
|
||||
end
|
||||
|
||||
# Prepare the inner (untrusted) HTML document
|
||||
untrusted_html = <<~HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>#{ERB::Util.html_escape(artifact.name)}</title>
|
||||
<style>
|
||||
#{artifact.css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
#{artifact.html}
|
||||
<script>
|
||||
#{artifact.js}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# Prepare the outer (trusted) HTML document
|
||||
trusted_html = <<~HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>#{ERB::Util.html_escape(artifact.name)}</title>
|
||||
<style>
|
||||
html, body, iframe {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
iframe {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
response.headers.delete("X-Frame-Options")
|
||||
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
|
||||
response.headers["X-Robots-Tag"] = "noindex"
|
||||
|
||||
# Render the content
|
||||
render html: trusted_html.html_safe, layout: false, content_type: "text/html"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_site_settings!
|
||||
if !SiteSetting.discourse_ai_enabled ||
|
||||
!SiteSetting.ai_artifact_security.in?(%w[lax strict])
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,14 @@ module DiscourseAi
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
requires_login
|
||||
|
||||
def show_debug_info_by_id
|
||||
log = AiApiAuditLog.find(params[:id])
|
||||
raise Discourse::NotFound if !log.topic
|
||||
|
||||
guardian.ensure_can_debug_ai_bot_conversation!(log.topic)
|
||||
render json: AiApiAuditLogSerializer.new(log, root: false), status: 200
|
||||
end
|
||||
|
||||
def show_debug_info
|
||||
post = Post.find(params[:post_id])
|
||||
guardian.ensure_can_debug_ai_bot_conversation!(post)
|
||||
|
||||
@@ -7,7 +7,8 @@ module DiscourseAi
|
||||
requires_login only: %i[create update destroy]
|
||||
before_action :require_site_settings!
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, only: %i[show]
|
||||
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
||||
skip_before_action :verify_authenticity_token, only: ["asset"]
|
||||
|
||||
def create
|
||||
ensure_allowed_create!
|
||||
@@ -50,6 +51,30 @@ module DiscourseAi
|
||||
end
|
||||
end
|
||||
|
||||
def asset
|
||||
no_cookies
|
||||
|
||||
name = params[:name]
|
||||
path, content_type =
|
||||
if name == "share"
|
||||
%w[share.css text/css]
|
||||
elsif name == "highlight"
|
||||
%w[highlight.min.js application/javascript]
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
|
||||
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
|
||||
|
||||
# note, path contains a ":version" which automatically busts the cache
|
||||
# based on file content, so this is safe
|
||||
response.headers["Last-Modified"] = 10.years.ago.httpdate
|
||||
response.headers["Content-Length"] = content.bytesize.to_s
|
||||
immutable_for 1.year
|
||||
|
||||
render plain: content, disposition: :nil, content_type: content_type
|
||||
end
|
||||
|
||||
def preview
|
||||
ensure_allowed_preview!
|
||||
data = SharedAiConversation.build_conversation_data(@topic, include_usernames: true)
|
||||
|
||||
@@ -35,7 +35,7 @@ module DiscourseAi
|
||||
prompt.custom_instruction = params[:custom_prompt]
|
||||
end
|
||||
|
||||
suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||
|
||||
hijack do
|
||||
render json:
|
||||
|
||||
@@ -2,13 +2,29 @@
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module SharedAiConversationsHelper
|
||||
# bump up version when assets change
|
||||
# long term we may want to change this cause it is hard to remember
|
||||
# to bump versions, but for now this does the job
|
||||
VERSION = "1"
|
||||
# keeping it here for caching
|
||||
def self.share_asset_url(asset_name)
|
||||
if !%w[share.css highlight.js].include?(asset_name)
|
||||
raise StandardError, "unknown asset type #{asset_name}"
|
||||
end
|
||||
|
||||
def share_asset_url(short_path)
|
||||
::UrlHelper.absolute("/plugins/discourse-ai/ai-share/#{short_path}?#{VERSION}")
|
||||
@urls ||= {}
|
||||
url = @urls[asset_name]
|
||||
return url if url
|
||||
|
||||
path = asset_name
|
||||
path = "highlight.min.js" if asset_name == "highlight.js"
|
||||
|
||||
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
|
||||
sha1 = Digest::SHA1.hexdigest(content)
|
||||
|
||||
url = "/discourse-ai/ai-bot/shared-ai-conversations/asset/#{sha1}/#{asset_name}"
|
||||
|
||||
@urls[asset_name] = GlobalPath.cdn_path(url)
|
||||
end
|
||||
|
||||
def share_asset_url(asset_name)
|
||||
DiscourseAi::AiBot::SharedAiConversationsHelper.share_asset_url(asset_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+3
-6
@@ -1,23 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ::Jobs
|
||||
class UpdateHotTopicGist < ::Jobs::Base
|
||||
class FastTrackTopicGist < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_summarization_enabled
|
||||
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
|
||||
return if !SiteSetting.ai_summary_gists_enabled
|
||||
|
||||
topic = Topic.find_by(id: args[:topic_id])
|
||||
return if topic.blank?
|
||||
|
||||
return if !TopicHotScore.where(topic: topic).exists?
|
||||
|
||||
summarizer = DiscourseAi::Summarization.topic_gist(topic)
|
||||
gist = summarizer.existing_summary
|
||||
return if gist.blank?
|
||||
return if !gist.outdated
|
||||
return if gist.present? && !gist.outdated
|
||||
|
||||
summarizer.force_summarize(Discourse.system_user)
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ::Jobs
|
||||
class HotTopicsGistBatch < ::Jobs::Base
|
||||
def execute(_args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_summarization_enabled
|
||||
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
|
||||
|
||||
Topic
|
||||
.joins("JOIN topic_hot_scores on topics.id = topic_hot_scores.topic_id")
|
||||
.order("topic_hot_scores.score DESC")
|
||||
.limit(SiteSetting.ai_summarize_max_hot_topics_gists_per_batch)
|
||||
.each do |topic|
|
||||
summarizer = DiscourseAi::Summarization.topic_gist(topic)
|
||||
gist = summarizer.existing_summary
|
||||
|
||||
summarizer.force_summarize(Discourse.system_user) if gist.blank? || gist.outdated
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -70,14 +70,15 @@ module Jobs
|
||||
Post
|
||||
.joins("LEFT JOIN #{table_name} ON #{table_name}.post_id = posts.id")
|
||||
.where(deleted_at: nil)
|
||||
.where(post_type: Post.types[:regular])
|
||||
.limit(limit - rebaked)
|
||||
|
||||
# First, we'll try to backfill embeddings for posts that have none
|
||||
posts
|
||||
.where("#{table_name}.post_id IS NULL")
|
||||
.find_each do |t|
|
||||
vector_rep.generate_representation_from(t)
|
||||
rebaked += 1
|
||||
.find_in_batches do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(batch)
|
||||
rebaked += batch.size
|
||||
end
|
||||
|
||||
return if rebaked >= limit
|
||||
@@ -90,24 +91,28 @@ module Jobs
|
||||
OR
|
||||
#{table_name}.strategy_version < #{strategy.version}
|
||||
SQL
|
||||
.find_each do |t|
|
||||
vector_rep.generate_representation_from(t)
|
||||
rebaked += 1
|
||||
.find_in_batches do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(batch)
|
||||
rebaked += batch.size
|
||||
end
|
||||
|
||||
return if rebaked >= limit
|
||||
|
||||
# Finally, we'll try to backfill embeddings for posts that have outdated
|
||||
# embeddings due to edits. Here we only do 10% of the limit
|
||||
posts
|
||||
.where("#{table_name}.updated_at < ?", 7.days.ago)
|
||||
.order("random()")
|
||||
.limit((limit - rebaked) / 10)
|
||||
.pluck(:id)
|
||||
.each do |id|
|
||||
vector_rep.generate_representation_from(Post.find_by(id: id))
|
||||
rebaked += 1
|
||||
end
|
||||
posts_batch_size = 1000
|
||||
|
||||
outdated_post_ids =
|
||||
posts
|
||||
.where("#{table_name}.updated_at < ?", 7.days.ago)
|
||||
.order("random()")
|
||||
.limit((limit - rebaked) / 10)
|
||||
.pluck(:id)
|
||||
|
||||
outdated_post_ids.each_slice(posts_batch_size) do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(Post.where(id: batch))
|
||||
rebaked += batch.length
|
||||
end
|
||||
|
||||
rebaked
|
||||
end
|
||||
@@ -120,14 +125,13 @@ module Jobs
|
||||
topics = topics.where("#{vector_rep.topic_table_name}.topic_id IS NULL") if !force
|
||||
|
||||
ids = topics.pluck("topics.id")
|
||||
batch_size = 1000
|
||||
|
||||
ids.each do |id|
|
||||
topic = Topic.find_by(id: id)
|
||||
if topic
|
||||
vector_rep.generate_representation_from(topic)
|
||||
done += 1
|
||||
end
|
||||
ids.each_slice(batch_size) do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(Topic.where(id: batch).order("topics.bumped_at DESC"))
|
||||
done += batch.length
|
||||
end
|
||||
|
||||
done
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,34 +10,50 @@ module ::Jobs
|
||||
return if !SiteSetting.ai_summarization_enabled
|
||||
return if SiteSetting.ai_summary_backfill_maximum_topics_per_hour.zero?
|
||||
|
||||
# Split budget in 12 intervals, but make sure is at least one.
|
||||
limit_per_job = [SiteSetting.ai_summary_backfill_maximum_topics_per_hour, 12].max / 12
|
||||
budget = [current_budget, limit_per_job].min
|
||||
system_user = Discourse.system_user
|
||||
|
||||
backfill_candidates
|
||||
.limit(budget)
|
||||
complete_t = AiSummary.summary_types[:complete]
|
||||
backfill_candidates(complete_t)
|
||||
.limit(current_budget(complete_t))
|
||||
.each do |topic|
|
||||
DiscourseAi::Summarization.topic_summary(topic).force_summarize(Discourse.system_user)
|
||||
DiscourseAi::Summarization.topic_summary(topic).force_summarize(system_user)
|
||||
end
|
||||
|
||||
return unless SiteSetting.ai_summary_gists_enabled
|
||||
|
||||
gist_t = AiSummary.summary_types[:gist]
|
||||
backfill_candidates(gist_t)
|
||||
.limit(current_budget(gist_t))
|
||||
.each { |topic| DiscourseAi::Summarization.topic_gist(topic).force_summarize(system_user) }
|
||||
end
|
||||
|
||||
def backfill_candidates
|
||||
def backfill_candidates(summary_type)
|
||||
Topic
|
||||
.where("topics.word_count >= ?", SiteSetting.ai_summary_backfill_minimum_word_count)
|
||||
.joins(
|
||||
"LEFT OUTER JOIN ai_summaries ais ON topics.id = ais.target_id AND ais.target_type = 'Topic'",
|
||||
)
|
||||
.joins(<<~SQL)
|
||||
LEFT OUTER JOIN ai_summaries ais ON
|
||||
topics.id = ais.target_id AND
|
||||
ais.target_type = 'Topic' AND
|
||||
ais.summary_type = '#{summary_type}'
|
||||
SQL
|
||||
.where(
|
||||
"ais.id IS NULL OR UPPER(ais.content_range) < topics.highest_post_number + 1",
|
||||
) # (1..1) gets stored ad (1..2).
|
||||
.order("ais.created_at DESC NULLS FIRST, topics.last_posted_at DESC")
|
||||
end
|
||||
|
||||
def current_budget
|
||||
def current_budget(type)
|
||||
# Split budget in 12 intervals, but make sure is at least one.
|
||||
base_budget = SiteSetting.ai_summary_backfill_maximum_topics_per_hour
|
||||
used_budget = AiSummary.complete.system.where("created_at > ?", 1.hour.ago).count
|
||||
limit_per_job = [base_budget, 12].max / 12
|
||||
|
||||
base_budget - used_budget
|
||||
used_budget =
|
||||
AiSummary.system.where("created_at > ?", 1.hour.ago).where(summary_type: type).count
|
||||
|
||||
current_budget = [(base_budget - used_budget), limit_per_job].min
|
||||
return 0 if current_budget < 0
|
||||
|
||||
current_budget
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,6 +13,15 @@ class AiApiAuditLog < ActiveRecord::Base
|
||||
Cohere = 6
|
||||
Ollama = 7
|
||||
SambaNova = 8
|
||||
Mistral = 9
|
||||
end
|
||||
|
||||
def next_log_id
|
||||
self.class.where("id > ?", id).where(topic_id: topic_id).order(id: :asc).pluck(:id).first
|
||||
end
|
||||
|
||||
def prev_log_id
|
||||
self.class.where("id < ?", id).where(topic_id: topic_id).order(id: :desc).pluck(:id).first
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AiArtifact < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :post
|
||||
validates :html, length: { maximum: 65_535 }
|
||||
validates :css, length: { maximum: 65_535 }
|
||||
validates :js, length: { maximum: 65_535 }
|
||||
|
||||
def self.iframe_for(id)
|
||||
<<~HTML
|
||||
<div class='ai-artifact'>
|
||||
<iframe src='#{url(id)}' frameborder="0" height="100%" width="100%"></iframe>
|
||||
<a href='#{url(id)}' target='_blank'>#{I18n.t("discourse_ai.ai_artifact.link")}</a>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
def self.url(id)
|
||||
Discourse.base_url + "/discourse-ai/ai-bot/artifacts/#{id}"
|
||||
end
|
||||
|
||||
def self.share_publicly(id:, post:)
|
||||
artifact = AiArtifact.find_by(id: id)
|
||||
artifact.update!(metadata: { public: true }) if artifact&.post&.topic&.id == post.topic.id
|
||||
end
|
||||
|
||||
def self.unshare_publicly(id:)
|
||||
artifact = AiArtifact.find_by(id: id)
|
||||
artifact&.update!(metadata: { public: false })
|
||||
end
|
||||
|
||||
def url
|
||||
self.class.url(id)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: ai_artifacts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :integer not null
|
||||
# post_id :integer not null
|
||||
# name :string(255) not null
|
||||
# html :string(65535)
|
||||
# css :string(65535)
|
||||
# js :string(65535)
|
||||
# metadata :jsonb
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
@@ -51,5 +51,6 @@ end
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_ai_summaries_on_target_type_and_target_id (target_type,target_id)
|
||||
# idx_on_target_id_target_type_summary_type_3355609fbb (target_id,target_type,summary_type) UNIQUE
|
||||
# index_ai_summaries_on_target_type_and_target_id (target_type,target_id)
|
||||
#
|
||||
|
||||
+20
-1
@@ -26,6 +26,16 @@ class LlmModel < ActiveRecord::Base
|
||||
},
|
||||
open_ai: {
|
||||
organization: :text,
|
||||
disable_native_tools: :checkbox,
|
||||
},
|
||||
mistral: {
|
||||
disable_native_tools: :checkbox,
|
||||
},
|
||||
google: {
|
||||
disable_native_tools: :checkbox,
|
||||
},
|
||||
azure: {
|
||||
disable_native_tools: :checkbox,
|
||||
},
|
||||
hugging_face: {
|
||||
disable_system_prompt: :checkbox,
|
||||
@@ -95,7 +105,16 @@ class LlmModel < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def seeded?
|
||||
id < 0
|
||||
id.present? && id < 0
|
||||
end
|
||||
|
||||
def api_key
|
||||
if seeded?
|
||||
env_key = "DISCOURSE_AI_SEEDED_LLM_API_KEY_#{id.abs}"
|
||||
ENV[env_key] || self[:api_key]
|
||||
else
|
||||
self[:api_key]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -172,6 +172,7 @@ end
|
||||
# updated_at :datetime not null
|
||||
# force_review :boolean default(FALSE), not null
|
||||
# reject_reason :text
|
||||
# potentially_illegal :boolean default(FALSE)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -229,6 +229,7 @@ end
|
||||
# updated_at :datetime not null
|
||||
# force_review :boolean default(FALSE), not null
|
||||
# reject_reason :text
|
||||
# potentially_illegal :boolean default(FALSE)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -34,6 +34,12 @@ class SharedAiConversation < ActiveRecord::Base
|
||||
|
||||
def self.destroy_conversation(conversation)
|
||||
conversation.destroy
|
||||
|
||||
maybe_topic = conversation.target
|
||||
if maybe_topic.is_a?(Topic)
|
||||
AiArtifact.where(post: maybe_topic.posts).update_all(metadata: { public: false })
|
||||
end
|
||||
|
||||
::Jobs.enqueue(
|
||||
:shared_conversation_adjust_upload_security,
|
||||
target_id: conversation.target_id,
|
||||
@@ -165,7 +171,7 @@ class SharedAiConversation < ActiveRecord::Base
|
||||
id: post.id,
|
||||
user_id: post.user_id,
|
||||
created_at: post.created_at,
|
||||
cooked: post.cooked,
|
||||
cooked: cook_artifacts(post),
|
||||
}
|
||||
|
||||
mapped[:persona] = persona if ai_bot_participant&.id == post.user_id
|
||||
@@ -175,6 +181,24 @@ class SharedAiConversation < ActiveRecord::Base
|
||||
}
|
||||
end
|
||||
|
||||
def self.cook_artifacts(post)
|
||||
html = post.cooked
|
||||
return html if !%w[lax strict].include?(SiteSetting.ai_artifact_security)
|
||||
|
||||
doc = Nokogiri::HTML5.fragment(html)
|
||||
doc
|
||||
.css("div.ai-artifact")
|
||||
.each do |node|
|
||||
id = node["data-ai-artifact-id"].to_i
|
||||
if id > 0
|
||||
AiArtifact.share_publicly(id: id, post: post)
|
||||
node.replace(AiArtifact.iframe_for(id))
|
||||
end
|
||||
end
|
||||
|
||||
doc.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def populate_user_info!(posts)
|
||||
|
||||
@@ -12,5 +12,7 @@ class AiApiAuditLogSerializer < ApplicationSerializer
|
||||
:post_id,
|
||||
:feature_name,
|
||||
:language_model,
|
||||
:created_at
|
||||
:created_at,
|
||||
:prev_log_id,
|
||||
:next_log_id
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<meta name="twitter:title" content="<%= I18n.t("discourse_ai.share_ai.title", title: @shared_conversation.title, site_name: SiteSetting.title) %>">
|
||||
<meta name="twitter:description" content="<%= @shared_conversation.formatted_excerpt %>">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="<%= ::UrlHelper.absolute("/plugins/discourse-ai/ai-share/share.css") %>">
|
||||
<link rel="stylesheet" href="<%= share_asset_url("share.css") %>">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
@@ -54,7 +54,7 @@
|
||||
</article>
|
||||
<% end %>
|
||||
</section>
|
||||
<script src="<%= share_asset_url("highlight.min.js") %>" nonce="<%= csp_nonce_placeholder %>" ></script>
|
||||
<script src="<%= share_asset_url("highlight.js") %>" nonce="<%= csp_nonce_placeholder %>" ></script>
|
||||
<script nonce="<%= csp_nonce_placeholder %>">
|
||||
document.querySelectorAll('pre code').forEach((el) => {
|
||||
hljs.highlightElement(el);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="cell title">
|
||||
{{#if this.model.icon}}
|
||||
{{d-icon this.model.icon}}
|
||||
{{/if}}
|
||||
{{this.model.title}}
|
||||
</div>
|
||||
|
||||
<div class="cell value today-count">
|
||||
<a href={{this.todayLink}}>
|
||||
{{number this.model.todayCount}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cell value yesterday-count {{this.model.yesterdayTrend}}"
|
||||
title={{this.model.yesterdayCountTitle}}
|
||||
>
|
||||
<a href={{this.yesterdayLink}}>
|
||||
{{number this.model.yesterdayCount}}
|
||||
</a>
|
||||
{{d-icon this.model.yesterdayTrendIcon}}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cell value sevendays-count {{this.model.sevenDaysTrend}}"
|
||||
title={{this.model.sevenDaysCountTitle}}
|
||||
>
|
||||
<a href={{this.lastSevenDaysLink}}>
|
||||
{{number this.model.lastSevenDaysCount}}
|
||||
</a>
|
||||
{{d-icon this.model.sevenDaysTrendIcon}}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cell value thirty-days-count {{this.model.thirtyDaysTrend}}"
|
||||
title={{this.model.thirtyDaysCountTitle}}
|
||||
>
|
||||
|
||||
<a href={{this.lastThirtyDaysLink}}>
|
||||
{{number this.model.lastThirtyDaysCount}}
|
||||
</a>
|
||||
{{#if this.model.canDisplayTrendIcon}}
|
||||
{{d-icon this.model.thirtyDaysTrendIcon}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
import Component from "@ember/component";
|
||||
import { attributeBindings, classNames } from "@ember-decorators/component";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
|
||||
@classNames("admin-report-counters")
|
||||
@attributeBindings("model.description:title")
|
||||
export default class AdminReportEmotion extends Component {
|
||||
get todayLink() {
|
||||
let date = moment().format("YYYY-MM-DD");
|
||||
return this._filterURL(date);
|
||||
}
|
||||
|
||||
get yesterdayLink() {
|
||||
let date = moment().subtract(1, "day").format("YYYY-MM-DD");
|
||||
return this._filterURL(date);
|
||||
}
|
||||
|
||||
get lastSevenDaysLink() {
|
||||
let date = moment().subtract(1, "week").format("YYYY-MM-DD");
|
||||
return this._filterURL(date);
|
||||
}
|
||||
|
||||
get lastThirtyDaysLink() {
|
||||
let date = moment().subtract(1, "month").format("YYYY-MM-DD");
|
||||
return this._filterURL(date);
|
||||
}
|
||||
|
||||
_baseFilter() {
|
||||
return "/filter?q=activity-after%3A";
|
||||
}
|
||||
|
||||
_model() {
|
||||
return "%20order%3A" + this.model.type;
|
||||
}
|
||||
|
||||
_filterURL(date) {
|
||||
return getURL(`${this._baseFilter()}${date}${this._model()}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import htmlClass from "discourse/helpers/html-class";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
|
||||
export default class AiArtifactComponent extends Component {
|
||||
@service siteSettings;
|
||||
@tracked expanded = false;
|
||||
@tracked showingArtifact = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.keydownHandler = this.handleKeydown.bind(this);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
window.removeEventListener("keydown", this.keydownHandler);
|
||||
}
|
||||
|
||||
@action
|
||||
handleKeydown(event) {
|
||||
if (event.key === "Escape" || event.key === "Esc") {
|
||||
this.expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
get requireClickToRun() {
|
||||
if (this.showingArtifact) {
|
||||
return false;
|
||||
}
|
||||
return this.siteSettings.ai_artifact_security === "strict";
|
||||
}
|
||||
|
||||
get artifactUrl() {
|
||||
return getURL(`/discourse-ai/ai-bot/artifacts/${this.args.artifactId}`);
|
||||
}
|
||||
|
||||
@action
|
||||
showArtifact() {
|
||||
this.showingArtifact = true;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleView() {
|
||||
this.expanded = !this.expanded;
|
||||
if (this.expanded) {
|
||||
window.addEventListener("keydown", this.keydownHandler);
|
||||
} else {
|
||||
window.removeEventListener("keydown", this.keydownHandler);
|
||||
}
|
||||
}
|
||||
|
||||
get wrapperClasses() {
|
||||
return `ai-artifact__wrapper ${
|
||||
this.expanded ? "ai-artifact__expanded" : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
@action
|
||||
artifactPanelHover() {
|
||||
// retrrigger animation
|
||||
const panel = document.querySelector(".ai-artifact__panel");
|
||||
panel.style.animation = "none"; // Stop the animation
|
||||
setTimeout(() => {
|
||||
panel.style.animation = ""; // Re-trigger the animation by removing the none style
|
||||
}, 0);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.expanded}}
|
||||
{{htmlClass "ai-artifact-expanded"}}
|
||||
{{/if}}
|
||||
<div class={{this.wrapperClasses}}>
|
||||
<div
|
||||
class="ai-artifact__panel--wrapper"
|
||||
{{on "mouseleave" this.artifactPanelHover}}
|
||||
>
|
||||
<div class="ai-artifact__panel">
|
||||
<DButton
|
||||
class="btn-flat btn-icon-text"
|
||||
@icon="discourse-compress"
|
||||
@label="discourse_ai.ai_artifact.collapse_view_label"
|
||||
@action={{this.toggleView}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.requireClickToRun}}
|
||||
<div class="ai-artifact__click-to-run">
|
||||
<DButton
|
||||
class="btn btn-primary"
|
||||
@icon="play"
|
||||
@label="discourse_ai.ai_artifact.click_to_run_label"
|
||||
@action={{this.showArtifact}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<iframe
|
||||
title="AI Artifact"
|
||||
src={{this.artifactUrl}}
|
||||
width="100%"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
{{/if}}
|
||||
{{#unless this.requireClickToRun}}
|
||||
<div class="ai-artifact__footer">
|
||||
<DButton
|
||||
class="btn-flat btn-icon-text ai-artifact__expand-button"
|
||||
@icon="discourse-expand"
|
||||
@label="discourse_ai.ai_artifact.expand_view_label"
|
||||
@action={{this.toggleView}}
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -4,35 +4,43 @@ import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import bodyClass from "discourse/helpers/body-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
|
||||
export default class AiGistToggle extends Component {
|
||||
@service router;
|
||||
@service gistPreference;
|
||||
|
||||
get shouldShow() {
|
||||
return this.router.currentRoute.attributes?.list?.topics?.some(
|
||||
(topic) => topic.ai_topic_gist
|
||||
);
|
||||
}
|
||||
@service gists;
|
||||
|
||||
get buttons() {
|
||||
return [
|
||||
{
|
||||
id: "gists_enabled",
|
||||
label: "discourse_ai.summarization.gists_enabled_long",
|
||||
icon: "discourse-sparkles",
|
||||
id: "table",
|
||||
label: "discourse_ai.summarization.topic_list_layout.button.compact",
|
||||
icon: "discourse-table",
|
||||
},
|
||||
{
|
||||
id: "gists_disabled",
|
||||
label: "discourse_ai.summarization.gists_disabled",
|
||||
icon: "far-eye-slash",
|
||||
id: "table-ai",
|
||||
label: "discourse_ai.summarization.topic_list_layout.button.expanded",
|
||||
icon: "discourse-table-sparkles",
|
||||
description:
|
||||
"discourse_ai.summarization.topic_list_layout.button.expanded_description",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get selectedOptionId() {
|
||||
return this.gists.get("preference");
|
||||
}
|
||||
|
||||
get currentButton() {
|
||||
const buttonPreference = this.buttons.find(
|
||||
(button) => button.id === this.selectedOptionId
|
||||
);
|
||||
return buttonPreference || this.buttons[0];
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(api) {
|
||||
this.dMenu = api;
|
||||
@@ -40,29 +48,22 @@ export default class AiGistToggle extends Component {
|
||||
|
||||
@action
|
||||
onSelect(optionId) {
|
||||
this.gistPreference.setPreference(optionId);
|
||||
this.gists.setPreference(optionId);
|
||||
this.dMenu.close();
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.shouldShow}}
|
||||
|
||||
{{#if this.gists.shouldShow}}
|
||||
{{bodyClass (concat "topic-list-layout-" this.gists.preference)}}
|
||||
<DMenu
|
||||
@modalForMobile={{true}}
|
||||
@autofocus={{true}}
|
||||
@identifier="ai-gists-dropdown"
|
||||
@identifier="topic-list-layout"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
@triggerClass="btn-transparent"
|
||||
@triggerClass="btn-default btn-icon"
|
||||
>
|
||||
<:trigger>
|
||||
<span class="d-button-label">
|
||||
{{i18n
|
||||
(concat
|
||||
"discourse_ai.summarization." this.gistPreference.preference
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
{{icon "angle-down"}}
|
||||
{{icon this.currentButton.icon}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
@@ -71,9 +72,17 @@ export default class AiGistToggle extends Component {
|
||||
<DButton
|
||||
@label={{button.label}}
|
||||
@icon={{button.icon}}
|
||||
class="btn-transparent"
|
||||
class="btn-transparent
|
||||
{{if button.description '--with-description'}}
|
||||
{{if (eq this.currentButton.id button.id) '--active'}}"
|
||||
@action={{fn this.onSelect button.id}}
|
||||
/>
|
||||
>
|
||||
{{#if button.description}}
|
||||
<div class="btn__description">
|
||||
{{i18n button.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</DButton>
|
||||
</dropdown.item>
|
||||
{{/each}}
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { on } from "@ember/modifier";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { later } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { eq } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import Avatar from "discourse/helpers/bound-avatar-template";
|
||||
|
||||
@@ -2,9 +2,8 @@ import Component from "@glimmer/component";
|
||||
import { concat, fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminPageSubheader from "admin/components/admin-page-subheader";
|
||||
@@ -124,8 +123,10 @@ export default class AiLlmsListEditor extends Component {
|
||||
<section class="ai-llms-list-editor__configured">
|
||||
<AdminPageSubheader
|
||||
@titleLabel="discourse_ai.llms.configured.title"
|
||||
@descriptionLabel="discourse_ai.llms.preconfigured.description"
|
||||
@learnMoreUrl="https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
|
||||
/>
|
||||
<table>
|
||||
<table class="d-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
|
||||
@@ -135,12 +136,19 @@ export default class AiLlmsListEditor extends Component {
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @llms as |llm|}}
|
||||
<tr data-llm-id={{llm.name}} class="ai-llm-list__row">
|
||||
<td class="column-name">
|
||||
<h3>{{llm.display_name}}</h3>
|
||||
<p>
|
||||
<tr
|
||||
data-llm-id={{llm.name}}
|
||||
class="ai-llm-list__row d-admin-row__content"
|
||||
>
|
||||
<td class="d-admin-row__overview">
|
||||
<div class="ai-llm-list__name">
|
||||
<strong>
|
||||
{{llm.display_name}}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ai-llm-list__description">
|
||||
{{this.modelDescription llm}}
|
||||
</p>
|
||||
</div>
|
||||
{{#if llm.used_by}}
|
||||
<ul class="ai-llm-list-editor__usages">
|
||||
{{#each llm.used_by as |usage|}}
|
||||
@@ -149,18 +157,20 @@ export default class AiLlmsListEditor extends Component {
|
||||
</ul>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="column-provider">
|
||||
<td class="d-admin-row__detail">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "discourse_ai.llms.provider"}}
|
||||
</div>
|
||||
{{i18n
|
||||
(concat "discourse_ai.llms.providers." llm.provider)
|
||||
}}
|
||||
</td>
|
||||
<td class="column-edit">
|
||||
<td class="d-admin-row__controls">
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-llms.show"
|
||||
class="btn btn-default"
|
||||
class="btn btn-default btn-small ai-llm-list__edit-button"
|
||||
@model={{llm.id}}
|
||||
>
|
||||
{{icon "wrench"}}
|
||||
<div class="d-button-label">
|
||||
{{i18n "discourse_ai.llms.edit"}}
|
||||
</div>
|
||||
@@ -173,7 +183,17 @@ export default class AiLlmsListEditor extends Component {
|
||||
</section>
|
||||
{{/if}}
|
||||
<section class="ai-llms-list-editor__templates">
|
||||
<AdminPageSubheader @titleLabel={{this.preconfiguredTitle}} />
|
||||
<AdminPageSubheader
|
||||
@titleLabel={{this.preconfiguredTitle}}
|
||||
@descriptionLabel={{unless
|
||||
this.hasLlmElements
|
||||
"discourse_ai.llms.preconfigured.description"
|
||||
}}
|
||||
@learnMoreUrl={{unless
|
||||
this.hasLlmElements
|
||||
"https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
|
||||
}}
|
||||
/>
|
||||
|
||||
<AdminSectionLandingWrapper
|
||||
class="ai-llms-list-editor__templates-list"
|
||||
|
||||
@@ -7,7 +7,7 @@ import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { later } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import BackButton from "discourse/components/back-button";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import Textarea from "discourse/components/d-textarea";
|
||||
|
||||
@@ -1,34 +1,20 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { cook } from "discourse/lib/text";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
import AdminPageSubheader from "admin/components/admin-page-subheader";
|
||||
import AiPersonaEditor from "./ai-persona-editor";
|
||||
|
||||
export default class AiPersonaListEditor extends Component {
|
||||
@service adminPluginNavManager;
|
||||
@tracked _noPersonaText = null;
|
||||
|
||||
get noPersonaText() {
|
||||
if (this._noPersonaText === null) {
|
||||
const raw = I18n.t("discourse_ai.ai_persona.no_persona_selected");
|
||||
cook(raw).then((result) => {
|
||||
this._noPersonaText = result;
|
||||
});
|
||||
}
|
||||
|
||||
return this._noPersonaText;
|
||||
}
|
||||
|
||||
@action
|
||||
async toggleEnabled(persona) {
|
||||
@@ -53,74 +39,76 @@ export default class AiPersonaListEditor extends Component {
|
||||
{{#if @currentPersona}}
|
||||
<AiPersonaEditor @model={{@currentPersona}} @personas={{@personas}} />
|
||||
{{else}}
|
||||
<div class="ai-persona-list-editor__header">
|
||||
<h3>{{i18n "discourse_ai.ai_persona.short_title"}}</h3>
|
||||
{{#unless @currentPersona.isNew}}
|
||||
<LinkTo
|
||||
<AdminPageSubheader
|
||||
@titleLabel="discourse_ai.ai_persona.short_title"
|
||||
@descriptionLabel="discourse_ai.ai_persona.persona_description"
|
||||
@learnMoreUrl="https://meta.discourse.org/t/ai-bot-personas/306099"
|
||||
>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary
|
||||
@label="discourse_ai.ai_persona.new"
|
||||
@route="adminPlugins.show.discourse-ai-personas.new"
|
||||
class="btn btn-small btn-primary"
|
||||
>
|
||||
{{icon "plus"}}
|
||||
<span>{{I18n.t "discourse_ai.ai_persona.new"}}</span>
|
||||
</LinkTo>
|
||||
{{/unless}}
|
||||
</div>
|
||||
@icon="plus"
|
||||
class="ai-persona-list-editor__new-button"
|
||||
/>
|
||||
</:actions>
|
||||
</AdminPageSubheader>
|
||||
|
||||
<div class="ai-persona-list-editor__empty">
|
||||
<details class="details__boxed">
|
||||
<summary>{{i18n
|
||||
"discourse_ai.ai_persona.what_are_personas"
|
||||
}}</summary>
|
||||
{{this.noPersonaText}}
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<table class="content-list ai-persona-list-editor">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
|
||||
<th>{{i18n "discourse_ai.ai_persona.enabled"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @personas as |persona|}}
|
||||
<tr
|
||||
data-persona-id={{persona.id}}
|
||||
class={{concatClass
|
||||
"ai-persona-list__row"
|
||||
(if persona.priority "priority")
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<div class="ai-persona-list__name-with-description">
|
||||
<div class="ai-persona-list__name">
|
||||
<strong>
|
||||
{{persona.name}}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ai-persona-list__description">
|
||||
{{persona.description}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<DToggleSwitch
|
||||
@state={{persona.enabled}}
|
||||
{{on "click" (fn this.toggleEnabled persona)}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-personas.show"
|
||||
@model={{persona}}
|
||||
class="btn btn-text btn-small"
|
||||
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
|
||||
</td>
|
||||
{{#if @personas}}
|
||||
<table class="content-list ai-persona-list-editor d-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
|
||||
<th>{{i18n "discourse_ai.ai_persona.enabled"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @personas as |persona|}}
|
||||
<tr
|
||||
data-persona-id={{persona.id}}
|
||||
class={{concatClass
|
||||
"ai-persona-list__row d-admin-row__content"
|
||||
(if persona.priority "priority")
|
||||
}}
|
||||
>
|
||||
<td class="d-admin-row__overview">
|
||||
<div class="ai-persona-list__name-with-description">
|
||||
<div class="ai-persona-list__name">
|
||||
<strong>
|
||||
{{persona.name}}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ai-persona-list__description">
|
||||
{{persona.description}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
<DToggleSwitch
|
||||
@state={{persona.enabled}}
|
||||
{{on "click" (fn this.toggleEnabled persona)}}
|
||||
/>
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-personas.show"
|
||||
@model={{persona}}
|
||||
class="btn btn-text btn-small"
|
||||
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@ctaLabel="discourse_ai.ai_persona.new"
|
||||
@ctaRoute="adminPlugins.show.discourse-ai-personas.new"
|
||||
@ctaClass="ai-persona-list-editor__empty-new-button"
|
||||
@emptyLabel="discourse_ai.ai_persona.no_personas"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { and } from "truth-helpers";
|
||||
import CookText from "discourse/components/cook-text";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user