Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/controllers/discourse_ai/ai_bot/artifacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ def show
<style>
#{artifact.css}
</style>
<script>
window._discourse_user_data = {
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
};
window.discourseArtifactReady = new Promise(resolve => {
window._resolveArtifactData = resolve;
});
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'discourse-artifact-data') {
window.discourseArtifactData = event.data.dataset || {};
Object.assign(window.discourseArtifactData, window._discourse_user_data);
window._resolveArtifactData(window.discourseArtifactData);
}
});
</script>
</head>
<body>
#{artifact.html}
Expand Down Expand Up @@ -74,6 +89,19 @@ def show
</head>
<body>
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
<script>
document.querySelector('iframe').addEventListener('load', function() {
try {
const iframeWindow = this.contentWindow;
const message = { type: 'discourse-artifact-data', dataset: {} };

if (window.frameElement && window.frameElement.dataset) {
Object.assign(message.dataset, window.frameElement.dataset);
}
iframeWindow.postMessage(message, '*');
} catch (e) { console.error('Error passing data to artifact:', e); }
});
</script>
</body>
</html>
HTML
Expand Down
11 changes: 11 additions & 0 deletions assets/javascripts/discourse/components/ai-artifact.gjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import htmlClass from "discourse/helpers/html-class";
Expand Down Expand Up @@ -88,6 +89,15 @@ export default class AiArtifactComponent extends Component {
}`;
}

@action
setDataAttributes(element) {
if (this.args.dataAttributes) {
Object.entries(this.args.dataAttributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
}
}

<template>
{{#if this.expanded}}
{{htmlClass "ai-artifact-expanded"}}
Expand Down Expand Up @@ -118,6 +128,7 @@ export default class AiArtifactComponent extends Component {
src={{this.artifactUrl}}
width="100%"
frameborder="0"
{{didInsert this.setDataAttributes}}
></iframe>
{{/if}}
{{#unless this.requireClickToRun}}
Expand Down
12 changes: 12 additions & 0 deletions assets/javascripts/initializers/ai-artifacts.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,24 @@ function initializeAiArtifacts(api) {
"data-ai-artifact-version"
);

const dataAttributes = {};
for (const attr of artifactElement.attributes) {
if (
attr.name.startsWith("data-") &&
attr.name !== "data-ai-artifact-id" &&
attr.name !== "data-ai-artifact-version"
) {
dataAttributes[attr.name] = attr.value;
}
}

helper.renderGlimmer(
artifactElement,
<template>
<AiArtifact
@artifactId={{artifactId}}
@artifactVersion={{artifactVersion}}
@dataAttributes={{dataAttributes}}
/>
</template>
);
Expand Down
79 changes: 79 additions & 0 deletions spec/system/ai_bot/artifact_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

RSpec.describe "AI Artifact with Data Attributes", type: :system do
fab!(:admin)
fab!(:user)
fab!(:author) { Fabricate(:user) }
fab!(:category) { Fabricate(:category, user: admin, read_restricted: false) }
fab!(:topic) { Fabricate(:topic, category: category, user: author) }
fab!(:post) { Fabricate(:post, topic: topic, user: author) }

before { SiteSetting.discourse_ai_enabled = true }

it "correctly passes data attributes and user info to a public AI artifact embedded in a post" do
artifact_js = <<~JS
window.discourseArtifactReady.then(data => {
const displayElement = document.getElementById('data-display');
if (displayElement) {
displayElement.innerText = JSON.stringify(data);
}
}).catch(err => {
const displayElement = document.getElementById('data-display');
if (displayElement) {
displayElement.innerText = 'Error: ' + err.message;
}
console.error("Artifact JS Error:", err);
});
JS

ai_artifact =
Fabricate(
:ai_artifact,
user: author,
name: "Data Passing Test Artifact",
html: "<div id='data-display'>Waiting for data...</div>",
js: artifact_js.strip,
metadata: {
public: true,
},
)

raw_post_content =
"<div class='ai-artifact' data-ai-artifact-id='#{ai_artifact.id}' data-custom-message='hello-from-post' data-post-author-id='#{author.id}'></div>"
_post = Fabricate(:post, topic: topic, user: author, raw: raw_post_content)

sign_in(user)
visit "/t/#{topic.slug}/#{topic.id}"

find(".ai-artifact__click-to-run button").click

artifact_element_selector = ".ai-artifact[data-ai-artifact-id='#{ai_artifact.id}']"
iframe_selector = "#{artifact_element_selector} iframe"

expect(page).to have_css(iframe_selector)

iframe_element = find(iframe_selector)
expect(iframe_element["data-custom-message"]).to eq("hello-from-post")
expect(iframe_element["data-post-author-id"]).to eq(author.id.to_s)

# note: artifacts are within nested iframes for security reasons
page.within_frame(iframe_element) do
inner_iframe = find("iframe")
page.within_frame(inner_iframe) do
data_display_element = find("#data-display")

expect(data_display_element.text).not_to be_empty
expect(data_display_element.text).not_to eq("Waiting for data...")
expect(data_display_element.text).not_to include("Error:")

artifact_data_json = data_display_element.text
artifact_data = JSON.parse(artifact_data_json)

expect(artifact_data["customMessage"]).to eq("hello-from-post")
expect(artifact_data["postAuthorId"]).to eq(author.id.to_s)

expect(artifact_data["username"]).to eq(user.username)
end
end
end
end
Loading