Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 7320f86

Browse files
committed
Merge branch 'main' into ai-helper-suggestions-refactor
2 parents 69d1486 + 54f2d34 commit 7320f86

File tree

236 files changed

+5835
-2744
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

236 files changed

+5835
-2744
lines changed

.eslintrc.cjs

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module AiBot
5+
class ArtifactsController < ApplicationController
6+
requires_plugin DiscourseAi::PLUGIN_NAME
7+
before_action :require_site_settings!
8+
9+
skip_before_action :preload_json, :check_xhr, only: %i[show]
10+
11+
def show
12+
artifact = AiArtifact.find(params[:id])
13+
14+
post = Post.find_by(id: artifact.post_id)
15+
if artifact.metadata&.dig("public")
16+
# no guardian needed
17+
else
18+
raise Discourse::NotFound if !post&.topic&.private_message?
19+
raise Discourse::NotFound if !guardian.can_see?(post)
20+
end
21+
22+
# Prepare the inner (untrusted) HTML document
23+
untrusted_html = <<~HTML
24+
<!DOCTYPE html>
25+
<html>
26+
<head>
27+
<meta charset="UTF-8">
28+
<title>#{ERB::Util.html_escape(artifact.name)}</title>
29+
<style>
30+
#{artifact.css}
31+
</style>
32+
</head>
33+
<body>
34+
#{artifact.html}
35+
<script>
36+
#{artifact.js}
37+
</script>
38+
</body>
39+
</html>
40+
HTML
41+
42+
# Prepare the outer (trusted) HTML document
43+
trusted_html = <<~HTML
44+
<!DOCTYPE html>
45+
<html>
46+
<head>
47+
<meta charset="UTF-8">
48+
<title>#{ERB::Util.html_escape(artifact.name)}</title>
49+
<style>
50+
html, body, iframe {
51+
margin: 0;
52+
padding: 0;
53+
width: 100%;
54+
height: 100%;
55+
border: 0;
56+
overflow: hidden;
57+
}
58+
iframe {
59+
overflow: auto;
60+
}
61+
</style>
62+
</head>
63+
<body>
64+
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
65+
</body>
66+
</html>
67+
HTML
68+
69+
response.headers.delete("X-Frame-Options")
70+
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
71+
response.headers["X-Robots-Tag"] = "noindex"
72+
73+
# Render the content
74+
render html: trusted_html.html_safe, layout: false, content_type: "text/html"
75+
end
76+
77+
private
78+
79+
def require_site_settings!
80+
if !SiteSetting.discourse_ai_enabled ||
81+
!SiteSetting.ai_artifact_security.in?(%w[lax strict])
82+
raise Discourse::NotFound
83+
end
84+
end
85+
end
86+
end
87+
end

app/controllers/discourse_ai/ai_bot/shared_ai_conversations_controller.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ class SharedAiConversationsController < ::ApplicationController
77
requires_login only: %i[create update destroy]
88
before_action :require_site_settings!
99

10-
skip_before_action :preload_json, :check_xhr, only: %i[show]
10+
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
11+
skip_before_action :verify_authenticity_token, only: ["asset"]
1112

1213
def create
1314
ensure_allowed_create!
@@ -50,6 +51,30 @@ def show
5051
end
5152
end
5253

54+
def asset
55+
no_cookies
56+
57+
name = params[:name]
58+
path, content_type =
59+
if name == "share"
60+
%w[share.css text/css]
61+
elsif name == "highlight"
62+
%w[highlight.min.js application/javascript]
63+
else
64+
raise Discourse::NotFound
65+
end
66+
67+
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
68+
69+
# note, path contains a ":version" which automatically busts the cache
70+
# based on file content, so this is safe
71+
response.headers["Last-Modified"] = 10.years.ago.httpdate
72+
response.headers["Content-Length"] = content.bytesize.to_s
73+
immutable_for 1.year
74+
75+
render plain: content, disposition: :nil, content_type: content_type
76+
end
77+
5378
def preview
5479
ensure_allowed_preview!
5580
data = SharedAiConversation.build_conversation_data(@topic, include_usernames: true)

app/helpers/discourse_ai/ai_bot/shared_ai_conversations_helper.rb

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,29 @@
22
module DiscourseAi
33
module AiBot
44
module SharedAiConversationsHelper
5-
# bump up version when assets change
6-
# long term we may want to change this cause it is hard to remember
7-
# to bump versions, but for now this does the job
8-
VERSION = "1"
5+
# keeping it here for caching
6+
def self.share_asset_url(asset_name)
7+
if !%w[share.css highlight.js].include?(asset_name)
8+
raise StandardError, "unknown asset type #{asset_name}"
9+
end
910

10-
def share_asset_url(short_path)
11-
::UrlHelper.absolute("/plugins/discourse-ai/ai-share/#{short_path}?#{VERSION}")
11+
@urls ||= {}
12+
url = @urls[asset_name]
13+
return url if url
14+
15+
path = asset_name
16+
path = "highlight.min.js" if asset_name == "highlight.js"
17+
18+
content = File.read(DiscourseAi.public_asset_path("ai-share/#{path}"))
19+
sha1 = Digest::SHA1.hexdigest(content)
20+
21+
url = "/discourse-ai/ai-bot/shared-ai-conversations/asset/#{sha1}/#{asset_name}"
22+
23+
@urls[asset_name] = GlobalPath.cdn_path(url)
24+
end
25+
26+
def share_asset_url(asset_name)
27+
DiscourseAi::AiBot::SharedAiConversationsHelper.share_asset_url(asset_name)
1228
end
1329
end
1430
end

app/models/ai_api_audit_log.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Provider
1313
Cohere = 6
1414
Ollama = 7
1515
SambaNova = 8
16+
Mistral = 9
1617
end
1718

1819
def next_log_id

app/models/ai_artifact.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
class AiArtifact < ActiveRecord::Base
4+
belongs_to :user
5+
belongs_to :post
6+
validates :html, length: { maximum: 65_535 }
7+
validates :css, length: { maximum: 65_535 }
8+
validates :js, length: { maximum: 65_535 }
9+
10+
def self.iframe_for(id)
11+
<<~HTML
12+
<div class='ai-artifact'>
13+
<iframe src='#{url(id)}' frameborder="0" height="100%" width="100%"></iframe>
14+
<a href='#{url(id)}' target='_blank'>#{I18n.t("discourse_ai.ai_artifact.link")}</a>
15+
</div>
16+
HTML
17+
end
18+
19+
def self.url(id)
20+
Discourse.base_url + "/discourse-ai/ai-bot/artifacts/#{id}"
21+
end
22+
23+
def self.share_publicly(id:, post:)
24+
artifact = AiArtifact.find_by(id: id)
25+
artifact.update!(metadata: { public: true }) if artifact&.post&.topic&.id == post.topic.id
26+
end
27+
28+
def self.unshare_publicly(id:)
29+
artifact = AiArtifact.find_by(id: id)
30+
artifact&.update!(metadata: { public: false })
31+
end
32+
33+
def url
34+
self.class.url(id)
35+
end
36+
end
37+
38+
# == Schema Information
39+
#
40+
# Table name: ai_artifacts
41+
#
42+
# id :bigint not null, primary key
43+
# user_id :integer not null
44+
# post_id :integer not null
45+
# name :string(255) not null
46+
# html :string(65535)
47+
# css :string(65535)
48+
# js :string(65535)
49+
# metadata :jsonb
50+
# created_at :datetime not null
51+
# updated_at :datetime not null
52+
#

app/models/ai_summary.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ def outdated
5151
#
5252
# Indexes
5353
#
54-
# index_ai_summaries_on_target_type_and_target_id (target_type,target_id)
54+
# idx_on_target_id_target_type_summary_type_3355609fbb (target_id,target_type,summary_type) UNIQUE
55+
# index_ai_summaries_on_target_type_and_target_id (target_type,target_id)
5556
#

app/models/completion_prompt.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class CompletionPrompt < ActiveRecord::Base
88
CUSTOM_PROMPT = -305
99
EXPLAIN = -306
1010
ILLUSTRATE_POST = -308
11+
DETECT_TEXT_LOCALE = -309
1112

1213
enum :prompt_type, { text: 0, list: 1, diff: 2 }
1314

app/models/llm_model.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ def self.provider_params
2626
},
2727
open_ai: {
2828
organization: :text,
29+
disable_native_tools: :checkbox,
30+
},
31+
mistral: {
32+
disable_native_tools: :checkbox,
33+
},
34+
google: {
35+
disable_native_tools: :checkbox,
36+
},
37+
azure: {
38+
disable_native_tools: :checkbox,
2939
},
3040
hugging_face: {
3141
disable_system_prompt: :checkbox,

app/models/shared_ai_conversation.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ def self.share_conversation(user, target, max_posts: DEFAULT_MAX_POSTS)
3434

3535
def self.destroy_conversation(conversation)
3636
conversation.destroy
37+
38+
maybe_topic = conversation.target
39+
if maybe_topic.is_a?(Topic)
40+
AiArtifact.where(post: maybe_topic.posts).update_all(metadata: { public: false })
41+
end
42+
3743
::Jobs.enqueue(
3844
:shared_conversation_adjust_upload_security,
3945
target_id: conversation.target_id,
@@ -165,7 +171,7 @@ def self.build_conversation_data(topic, max_posts: DEFAULT_MAX_POSTS, include_us
165171
id: post.id,
166172
user_id: post.user_id,
167173
created_at: post.created_at,
168-
cooked: post.cooked,
174+
cooked: cook_artifacts(post),
169175
}
170176

171177
mapped[:persona] = persona if ai_bot_participant&.id == post.user_id
@@ -175,6 +181,24 @@ def self.build_conversation_data(topic, max_posts: DEFAULT_MAX_POSTS, include_us
175181
}
176182
end
177183

184+
def self.cook_artifacts(post)
185+
html = post.cooked
186+
return html if !%w[lax strict].include?(SiteSetting.ai_artifact_security)
187+
188+
doc = Nokogiri::HTML5.fragment(html)
189+
doc
190+
.css("div.ai-artifact")
191+
.each do |node|
192+
id = node["data-ai-artifact-id"].to_i
193+
if id > 0
194+
AiArtifact.share_publicly(id: id, post: post)
195+
node.replace(AiArtifact.iframe_for(id))
196+
end
197+
end
198+
199+
doc.to_s
200+
end
201+
178202
private
179203

180204
def populate_user_info!(posts)

0 commit comments

Comments
 (0)