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.
This commit is contained in:
Sam
2025-06-12 20:04:48 +10:00
committed by GitHub
parent b5a2ee31ab
commit 02bc9f645e
9 changed files with 83 additions and 11 deletions
@@ -335,7 +335,7 @@ module DiscourseAi
def require_site_settings!
if !SiteSetting.discourse_ai_enabled ||
!SiteSetting.ai_artifact_security.in?(%w[lax strict])
!SiteSetting.ai_artifact_security.in?(%w[lax hybrid strict])
raise Discourse::NotFound
end
end
+1 -1
View File
@@ -178,7 +178,7 @@ class SharedAiConversation < ActiveRecord::Base
def self.cook_artifacts(post)
html = post.cooked
return html if !%w[lax strict].include?(SiteSetting.ai_artifact_security)
return html if !%w[lax hybrid strict].include?(SiteSetting.ai_artifact_security)
doc = Nokogiri::HTML5.fragment(html)
doc
@@ -3,6 +3,7 @@ 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 { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import htmlClass from "discourse/helpers/html-class";
import getURL from "discourse/lib/get-url";
@@ -51,7 +52,21 @@ export default class AiArtifactComponent extends Component {
if (this.showingArtifact) {
return false;
}
return this.siteSettings.ai_artifact_security === "strict";
if (this.siteSettings.ai_artifact_security === "strict") {
return true;
}
if (this.siteSettings.ai_artifact_security === "hybrid") {
const shouldAutorun =
this.args.autorun === "true" ||
this.args.autorun === true ||
this.args.autorun === "1";
return !shouldAutorun;
}
return this.siteSettings.ai_artifact_security !== "lax";
}
get artifactUrl() {
@@ -86,7 +101,7 @@ export default class AiArtifactComponent extends Component {
get wrapperClasses() {
return `ai-artifact__wrapper ${
this.expanded ? "ai-artifact__expanded" : ""
}`;
} ${this.seamless ? "ai-artifact__seamless" : ""}`;
}
@action
@@ -98,11 +113,38 @@ export default class AiArtifactComponent extends Component {
}
}
get heightStyle() {
if (this.args.artifactHeight) {
let height = parseInt(this.args.artifactHeight, 10);
if (isNaN(height) || height <= 0) {
height = 500; // default height if the provided value is invalid
}
if (height > 2000) {
height = 2000; // cap the height to a maximum of 2000px
}
return htmlSafe(`height: ${height}px;`);
}
}
get seamless() {
return (
this.args.seamless === "true" ||
this.args.seamless === true ||
this.args.seamless === "1"
);
}
get showFooter() {
return !this.seamless && !this.requireClickToRun;
}
<template>
{{#if this.expanded}}
{{htmlClass "ai-artifact-expanded"}}
{{/if}}
<div class={{this.wrapperClasses}}>
<div class={{this.wrapperClasses}} style={{this.heightStyle}}>
<div class="ai-artifact__panel--wrapper">
<div class="ai-artifact__panel">
<DButton
@@ -131,7 +173,7 @@ export default class AiArtifactComponent extends Component {
{{didInsert this.setDataAttributes}}
></iframe>
{{/if}}
{{#unless this.requireClickToRun}}
{{#if this.showFooter}}
<div class="ai-artifact__footer">
<DButton
class="btn-transparent btn-icon-text ai-artifact__expand-button"
@@ -140,7 +182,7 @@ export default class AiArtifactComponent extends Component {
@action={{this.toggleView}}
/>
</div>
{{/unless}}
{{/if}}
</div>
</template>
}
@@ -18,12 +18,27 @@ function initializeAiArtifacts(api) {
"data-ai-artifact-version"
);
const artifactHeight = artifactElement.getAttribute(
"data-ai-artifact-height"
);
const autorun =
artifactElement.getAttribute("data-ai-artifact-autorun") ||
artifactElement.hasAttribute("data-ai-artifact-autorun");
const seamless =
artifactElement.getAttribute("data-ai-artifact-seamless") ||
artifactElement.hasAttribute("data-ai-artifact-seamless");
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"
attr.name !== "data-ai-artifact-version" &&
attr.name !== "data-ai-artifact-height" &&
attr.name !== "data-ai-artifact-autorun" &&
attr.name !== "data-ai-artifact-seamless"
) {
dataAttributes[attr.name] = attr.value;
}
@@ -35,6 +50,9 @@ function initializeAiArtifacts(api) {
<AiArtifact
@artifactId={{artifactId}}
@artifactVersion={{artifactVersion}}
@artifactHeight={{artifactHeight}}
@autorun={{autorun}}
@seamless={{seamless}}
@dataAttributes={{dataAttributes}}
/>
</template>
@@ -4,5 +4,8 @@ export function setup(helper) {
"div[class=ai-artifact]",
"div[data-ai-artifact-id]",
"div[data-ai-artifact-version]",
"div[data-ai-artifact-autorun]",
"div[data-ai-artifact-height]",
"div[data-ai-artifact-width]",
]);
}
@@ -20,7 +20,15 @@
height: calc(100% - 2em);
}
&:not(.ai-artifact__expanded) {
&.ai-artifact__seamless {
padding-bottom: 1em;
iframe {
height: 100%;
}
}
&:not(.ai-artifact__expanded, .ai-artifact__seamless) {
iframe {
box-shadow: var(--shadow-card);
}
+1 -1
View File
@@ -25,7 +25,7 @@ en:
description: "Periodic report based on a large language model"
site_settings:
discourse_ai_enabled: "Enable the discourse AI plugin."
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."
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."
ai_toxicity_enabled: "Enable the toxicity module."
ai_toxicity_inference_service_api_endpoint: "URL where the API is running for the toxicity module"
ai_toxicity_inference_service_api_key: "API key for the toxicity API"
+1
View File
@@ -9,6 +9,7 @@ discourse_ai:
choices:
- "disabled"
- "lax"
- "hybrid"
- "strict"
ai_sentiment_enabled:
+1 -1
View File
@@ -119,7 +119,7 @@ module DiscourseAi
Tools::Researcher,
]
if SiteSetting.ai_artifact_security.in?(%w[lax strict])
if SiteSetting.ai_artifact_security.in?(%w[lax hybrid strict])
tools << Tools::CreateArtifact
tools << Tools::UpdateArtifact
tools << Tools::ReadArtifact