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

Commit 3ac2359

Browse files
authored
FEATURE: allow passing in data attributes to an artifact (#1346)
Also allow artifact access to current username Usage inside artifact is: 1. await window.discourseArtifactReady; 2. access data via window.discourseArtifactData;
1 parent 925949d commit 3ac2359

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed

app/controllers/discourse_ai/ai_bot/artifacts_controller.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ def show
4242
<style>
4343
#{artifact.css}
4444
</style>
45+
<script>
46+
window._discourse_user_data = {
47+
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
48+
};
49+
window.discourseArtifactReady = new Promise(resolve => {
50+
window._resolveArtifactData = resolve;
51+
});
52+
window.addEventListener('message', function(event) {
53+
if (event.data && event.data.type === 'discourse-artifact-data') {
54+
window.discourseArtifactData = event.data.dataset || {};
55+
Object.assign(window.discourseArtifactData, window._discourse_user_data);
56+
window._resolveArtifactData(window.discourseArtifactData);
57+
}
58+
});
59+
</script>
4560
</head>
4661
<body>
4762
#{artifact.html}
@@ -74,6 +89,19 @@ def show
7489
</head>
7590
<body>
7691
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
92+
<script>
93+
document.querySelector('iframe').addEventListener('load', function() {
94+
try {
95+
const iframeWindow = this.contentWindow;
96+
const message = { type: 'discourse-artifact-data', dataset: {} };
97+
98+
if (window.frameElement && window.frameElement.dataset) {
99+
Object.assign(message.dataset, window.frameElement.dataset);
100+
}
101+
iframeWindow.postMessage(message, '*');
102+
} catch (e) { console.error('Error passing data to artifact:', e); }
103+
});
104+
</script>
77105
</body>
78106
</html>
79107
HTML

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Component from "@glimmer/component";
22
import { tracked } from "@glimmer/tracking";
33
import { action } from "@ember/object";
4+
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
45
import { service } from "@ember/service";
56
import DButton from "discourse/components/d-button";
67
import htmlClass from "discourse/helpers/html-class";
@@ -88,6 +89,15 @@ export default class AiArtifactComponent extends Component {
8889
}`;
8990
}
9091

92+
@action
93+
setDataAttributes(element) {
94+
if (this.args.dataAttributes) {
95+
Object.entries(this.args.dataAttributes).forEach(([key, value]) => {
96+
element.setAttribute(key, value);
97+
});
98+
}
99+
}
100+
91101
<template>
92102
{{#if this.expanded}}
93103
{{htmlClass "ai-artifact-expanded"}}
@@ -118,6 +128,7 @@ export default class AiArtifactComponent extends Component {
118128
src={{this.artifactUrl}}
119129
width="100%"
120130
frameborder="0"
131+
{{didInsert this.setDataAttributes}}
121132
></iframe>
122133
{{/if}}
123134
{{#unless this.requireClickToRun}}

assets/javascripts/initializers/ai-artifacts.gjs

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

21+
const dataAttributes = {};
22+
for (const attr of artifactElement.attributes) {
23+
if (
24+
attr.name.startsWith("data-") &&
25+
attr.name !== "data-ai-artifact-id" &&
26+
attr.name !== "data-ai-artifact-version"
27+
) {
28+
dataAttributes[attr.name] = attr.value;
29+
}
30+
}
31+
2132
helper.renderGlimmer(
2233
artifactElement,
2334
<template>
2435
<AiArtifact
2536
@artifactId={{artifactId}}
2637
@artifactVersion={{artifactVersion}}
38+
@dataAttributes={{dataAttributes}}
2739
/>
2840
</template>
2941
);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe "AI Artifact with Data Attributes", type: :system do
4+
fab!(:admin)
5+
fab!(:user)
6+
fab!(:author) { Fabricate(:user) }
7+
fab!(:category) { Fabricate(:category, user: admin, read_restricted: false) }
8+
fab!(:topic) { Fabricate(:topic, category: category, user: author) }
9+
fab!(:post) { Fabricate(:post, topic: topic, user: author) }
10+
11+
before { SiteSetting.discourse_ai_enabled = true }
12+
13+
it "correctly passes data attributes and user info to a public AI artifact embedded in a post" do
14+
artifact_js = <<~JS
15+
window.discourseArtifactReady.then(data => {
16+
const displayElement = document.getElementById('data-display');
17+
if (displayElement) {
18+
displayElement.innerText = JSON.stringify(data);
19+
}
20+
}).catch(err => {
21+
const displayElement = document.getElementById('data-display');
22+
if (displayElement) {
23+
displayElement.innerText = 'Error: ' + err.message;
24+
}
25+
console.error("Artifact JS Error:", err);
26+
});
27+
JS
28+
29+
ai_artifact =
30+
Fabricate(
31+
:ai_artifact,
32+
user: author,
33+
name: "Data Passing Test Artifact",
34+
html: "<div id='data-display'>Waiting for data...</div>",
35+
js: artifact_js.strip,
36+
metadata: {
37+
public: true,
38+
},
39+
)
40+
41+
raw_post_content =
42+
"<div class='ai-artifact' data-ai-artifact-id='#{ai_artifact.id}' data-custom-message='hello-from-post' data-post-author-id='#{author.id}'></div>"
43+
_post = Fabricate(:post, topic: topic, user: author, raw: raw_post_content)
44+
45+
sign_in(user)
46+
visit "/t/#{topic.slug}/#{topic.id}"
47+
48+
find(".ai-artifact__click-to-run button").click
49+
50+
artifact_element_selector = ".ai-artifact[data-ai-artifact-id='#{ai_artifact.id}']"
51+
iframe_selector = "#{artifact_element_selector} iframe"
52+
53+
expect(page).to have_css(iframe_selector)
54+
55+
iframe_element = find(iframe_selector)
56+
expect(iframe_element["data-custom-message"]).to eq("hello-from-post")
57+
expect(iframe_element["data-post-author-id"]).to eq(author.id.to_s)
58+
59+
# note: artifacts are within nested iframes for security reasons
60+
page.within_frame(iframe_element) do
61+
inner_iframe = find("iframe")
62+
page.within_frame(inner_iframe) do
63+
data_display_element = find("#data-display")
64+
65+
expect(data_display_element.text).not_to be_empty
66+
expect(data_display_element.text).not_to eq("Waiting for data...")
67+
expect(data_display_element.text).not_to include("Error:")
68+
69+
artifact_data_json = data_display_element.text
70+
artifact_data = JSON.parse(artifact_data_json)
71+
72+
expect(artifact_data["customMessage"]).to eq("hello-from-post")
73+
expect(artifact_data["postAuthorId"]).to eq(author.id.to_s)
74+
75+
expect(artifact_data["username"]).to eq(user.username)
76+
end
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)