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

Commit 02bc9f6

Browse files
authored
FEATURE: hybrid artifact security mode (#1431)
In hybrid mode ai artifacts can optionally automatically run. This is useful for cases where you may want to embed a survey and so on. Additionally, artifacts now allow for better fidelity around display: <div class="ai-artifact" data-ai-artifact-id="501" data-ai-artifact-height="300px" data-ai-artifact-autorun data-ai-artifact-seamless></div> User can supply height and seamless mode to be seamlessly rendered with no box shadow and show full screen button.
1 parent b5a2ee3 commit 02bc9f6

File tree

9 files changed

+83
-11
lines changed

9 files changed

+83
-11
lines changed

app/controllers/discourse_ai/ai_bot/artifacts_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ def set_security_headers
335335

336336
def require_site_settings!
337337
if !SiteSetting.discourse_ai_enabled ||
338-
!SiteSetting.ai_artifact_security.in?(%w[lax strict])
338+
!SiteSetting.ai_artifact_security.in?(%w[lax hybrid strict])
339339
raise Discourse::NotFound
340340
end
341341
end

app/models/shared_ai_conversation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def self.build_conversation_data(topic, max_posts: DEFAULT_MAX_POSTS, include_us
178178

179179
def self.cook_artifacts(post)
180180
html = post.cooked
181-
return html if !%w[lax strict].include?(SiteSetting.ai_artifact_security)
181+
return html if !%w[lax hybrid strict].include?(SiteSetting.ai_artifact_security)
182182

183183
doc = Nokogiri::HTML5.fragment(html)
184184
doc

assets/javascripts/discourse/components/ai-artifact.gjs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { tracked } from "@glimmer/tracking";
33
import { action } from "@ember/object";
44
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
55
import { service } from "@ember/service";
6+
import { htmlSafe } from "@ember/template";
67
import DButton from "discourse/components/d-button";
78
import htmlClass from "discourse/helpers/html-class";
89
import getURL from "discourse/lib/get-url";
@@ -51,7 +52,21 @@ export default class AiArtifactComponent extends Component {
5152
if (this.showingArtifact) {
5253
return false;
5354
}
54-
return this.siteSettings.ai_artifact_security === "strict";
55+
56+
if (this.siteSettings.ai_artifact_security === "strict") {
57+
return true;
58+
}
59+
60+
if (this.siteSettings.ai_artifact_security === "hybrid") {
61+
const shouldAutorun =
62+
this.args.autorun === "true" ||
63+
this.args.autorun === true ||
64+
this.args.autorun === "1";
65+
66+
return !shouldAutorun;
67+
}
68+
69+
return this.siteSettings.ai_artifact_security !== "lax";
5570
}
5671

5772
get artifactUrl() {
@@ -86,7 +101,7 @@ export default class AiArtifactComponent extends Component {
86101
get wrapperClasses() {
87102
return `ai-artifact__wrapper ${
88103
this.expanded ? "ai-artifact__expanded" : ""
89-
}`;
104+
} ${this.seamless ? "ai-artifact__seamless" : ""}`;
90105
}
91106

92107
@action
@@ -98,11 +113,38 @@ export default class AiArtifactComponent extends Component {
98113
}
99114
}
100115

116+
get heightStyle() {
117+
if (this.args.artifactHeight) {
118+
let height = parseInt(this.args.artifactHeight, 10);
119+
if (isNaN(height) || height <= 0) {
120+
height = 500; // default height if the provided value is invalid
121+
}
122+
123+
if (height > 2000) {
124+
height = 2000; // cap the height to a maximum of 2000px
125+
}
126+
127+
return htmlSafe(`height: ${height}px;`);
128+
}
129+
}
130+
131+
get seamless() {
132+
return (
133+
this.args.seamless === "true" ||
134+
this.args.seamless === true ||
135+
this.args.seamless === "1"
136+
);
137+
}
138+
139+
get showFooter() {
140+
return !this.seamless && !this.requireClickToRun;
141+
}
142+
101143
<template>
102144
{{#if this.expanded}}
103145
{{htmlClass "ai-artifact-expanded"}}
104146
{{/if}}
105-
<div class={{this.wrapperClasses}}>
147+
<div class={{this.wrapperClasses}} style={{this.heightStyle}}>
106148
<div class="ai-artifact__panel--wrapper">
107149
<div class="ai-artifact__panel">
108150
<DButton
@@ -131,7 +173,7 @@ export default class AiArtifactComponent extends Component {
131173
{{didInsert this.setDataAttributes}}
132174
></iframe>
133175
{{/if}}
134-
{{#unless this.requireClickToRun}}
176+
{{#if this.showFooter}}
135177
<div class="ai-artifact__footer">
136178
<DButton
137179
class="btn-transparent btn-icon-text ai-artifact__expand-button"
@@ -140,7 +182,7 @@ export default class AiArtifactComponent extends Component {
140182
@action={{this.toggleView}}
141183
/>
142184
</div>
143-
{{/unless}}
185+
{{/if}}
144186
</div>
145187
</template>
146188
}

assets/javascripts/initializers/ai-artifacts.gjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,27 @@ function initializeAiArtifacts(api) {
1818
"data-ai-artifact-version"
1919
);
2020

21+
const artifactHeight = artifactElement.getAttribute(
22+
"data-ai-artifact-height"
23+
);
24+
25+
const autorun =
26+
artifactElement.getAttribute("data-ai-artifact-autorun") ||
27+
artifactElement.hasAttribute("data-ai-artifact-autorun");
28+
29+
const seamless =
30+
artifactElement.getAttribute("data-ai-artifact-seamless") ||
31+
artifactElement.hasAttribute("data-ai-artifact-seamless");
32+
2133
const dataAttributes = {};
2234
for (const attr of artifactElement.attributes) {
2335
if (
2436
attr.name.startsWith("data-") &&
2537
attr.name !== "data-ai-artifact-id" &&
26-
attr.name !== "data-ai-artifact-version"
38+
attr.name !== "data-ai-artifact-version" &&
39+
attr.name !== "data-ai-artifact-height" &&
40+
attr.name !== "data-ai-artifact-autorun" &&
41+
attr.name !== "data-ai-artifact-seamless"
2742
) {
2843
dataAttributes[attr.name] = attr.value;
2944
}
@@ -35,6 +50,9 @@ function initializeAiArtifacts(api) {
3550
<AiArtifact
3651
@artifactId={{artifactId}}
3752
@artifactVersion={{artifactVersion}}
53+
@artifactHeight={{artifactHeight}}
54+
@autorun={{autorun}}
55+
@seamless={{seamless}}
3856
@dataAttributes={{dataAttributes}}
3957
/>
4058
</template>

assets/javascripts/lib/discourse-markdown/ai-tags.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ export function setup(helper) {
44
"div[class=ai-artifact]",
55
"div[data-ai-artifact-id]",
66
"div[data-ai-artifact-version]",
7+
"div[data-ai-artifact-autorun]",
8+
"div[data-ai-artifact-height]",
9+
"div[data-ai-artifact-width]",
710
]);
811
}

assets/stylesheets/modules/ai-bot/common/ai-artifact.scss

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
height: calc(100% - 2em);
2121
}
2222

23-
&:not(.ai-artifact__expanded) {
23+
&.ai-artifact__seamless {
24+
padding-bottom: 1em;
25+
26+
iframe {
27+
height: 100%;
28+
}
29+
}
30+
31+
&:not(.ai-artifact__expanded, .ai-artifact__seamless) {
2432
iframe {
2533
box-shadow: var(--shadow-card);
2634
}

config/locales/server.en.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ en:
2525
description: "Periodic report based on a large language model"
2626
site_settings:
2727
discourse_ai_enabled: "Enable the discourse AI plugin."
28-
ai_artifact_security: "The AI artifact system generates IFRAMEs with runnable code. Strict mode disables sharing and forces an extra click to run code. Lax mode allows sharing of artifacts and runs code directly. Disabled mode disables the artifact system."
28+
ai_artifact_security: "The AI artifact system generates IFRAMEs with runnable code. Strict mode forces an extra click to run code. Lax mode runs code immediately. Hybrid mode allows user to supply data-ai-artifact-autorun to show right away. Disabled mode disables the artifact system."
2929
ai_toxicity_enabled: "Enable the toxicity module."
3030
ai_toxicity_inference_service_api_endpoint: "URL where the API is running for the toxicity module"
3131
ai_toxicity_inference_service_api_key: "API key for the toxicity API"

config/settings.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ discourse_ai:
99
choices:
1010
- "disabled"
1111
- "lax"
12+
- "hybrid"
1213
- "strict"
1314

1415
ai_sentiment_enabled:

lib/personas/persona.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def all_available_tools
119119
Tools::Researcher,
120120
]
121121

122-
if SiteSetting.ai_artifact_security.in?(%w[lax strict])
122+
if SiteSetting.ai_artifact_security.in?(%w[lax hybrid strict])
123123
tools << Tools::CreateArtifact
124124
tools << Tools::UpdateArtifact
125125
tools << Tools::ReadArtifact

0 commit comments

Comments
 (0)