diff --git a/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb b/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb index d5b6db76..9ce89cef 100644 --- a/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb +++ b/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb @@ -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 diff --git a/app/models/shared_ai_conversation.rb b/app/models/shared_ai_conversation.rb index 60e852a3..a4485e06 100644 --- a/app/models/shared_ai_conversation.rb +++ b/app/models/shared_ai_conversation.rb @@ -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 diff --git a/assets/javascripts/discourse/components/ai-artifact.gjs b/assets/javascripts/discourse/components/ai-artifact.gjs index a9ccfbc3..fc7a0e0b 100644 --- a/assets/javascripts/discourse/components/ai-artifact.gjs +++ b/assets/javascripts/discourse/components/ai-artifact.gjs @@ -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; + } + } diff --git a/assets/javascripts/initializers/ai-artifacts.gjs b/assets/javascripts/initializers/ai-artifacts.gjs index 477a98ef..a7903188 100644 --- a/assets/javascripts/initializers/ai-artifacts.gjs +++ b/assets/javascripts/initializers/ai-artifacts.gjs @@ -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) { diff --git a/assets/javascripts/lib/discourse-markdown/ai-tags.js b/assets/javascripts/lib/discourse-markdown/ai-tags.js index 0532a105..492e5540 100644 --- a/assets/javascripts/lib/discourse-markdown/ai-tags.js +++ b/assets/javascripts/lib/discourse-markdown/ai-tags.js @@ -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]", ]); } diff --git a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss index 14a283c8..8867c45f 100644 --- a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss +++ b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss @@ -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); } diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b219437c..e816125d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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" diff --git a/config/settings.yml b/config/settings.yml index c9566864..42128dee 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -9,6 +9,7 @@ discourse_ai: choices: - "disabled" - "lax" + - "hybrid" - "strict" ai_sentiment_enabled: diff --git a/lib/personas/persona.rb b/lib/personas/persona.rb index 53170bcc..27e9c3b0 100644 --- a/lib/personas/persona.rb +++ b/lib/personas/persona.rb @@ -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