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

Commit 0191b41

Browse files
committed
FEATURE: AI artifacts
Initial implementation of an artifact system which allows users to generate HTML pages directly from the AI persona.
1 parent bffe9df commit 0191b41

File tree

11 files changed

+367
-0
lines changed

11 files changed

+367
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module AiBot
5+
class ArtifactsController < ApplicationController
6+
7+
requires_plugin DiscourseAi::PLUGIN_NAME
8+
9+
skip_before_action :preload_json, :check_xhr, only: %i[show]
10+
11+
def show
12+
artifact = AiArtifact.find(params[:id])
13+
14+
post = Post.find_by(id: artifact.post_id)
15+
raise Discourse::NotFound unless post && guardian.can_see?(post)
16+
17+
# Prepare the HTML document
18+
html = <<~HTML
19+
<!DOCTYPE html>
20+
<html>
21+
<head>
22+
<meta charset="UTF-8">
23+
<title>#{ERB::Util.html_escape(artifact.name)}</title>
24+
<style>
25+
#{artifact.css}
26+
</style>
27+
</head>
28+
<body>
29+
#{artifact.html}
30+
<script>
31+
#{artifact.js}
32+
</script>
33+
</body>
34+
</html>
35+
HTML
36+
37+
response.headers.delete("X-Frame-Options")
38+
response.headers.delete("Content-Security-Policy")
39+
40+
# Render the content
41+
render html: html.html_safe, layout: false, content_type: "text/html"
42+
end
43+
end
44+
end
45+
end

app/models/ai_artifact.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
class AiArtifact < ActiveRecord::Base
4+
belongs_to :user
5+
belongs_to :post
6+
end
7+
8+
# == Schema Information
9+
#
10+
# Table name: ai_artifacts
11+
#
12+
# id :bigint not null, primary key
13+
# user_id :integer not null
14+
# post_id :integer not null
15+
# name :string(255) not null
16+
# html :string(65535)
17+
# css :string(65535)
18+
# js :string(65535)
19+
# metadata :jsonb
20+
# created_at :datetime not null
21+
# updated_at :datetime not null
22+
#
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { withPluginApi } from "discourse/lib/plugin-api";
2+
3+
function initializeAiArtifactTabs(api) {
4+
api.decorateCooked(
5+
($element) => {
6+
const element = $element[0];
7+
const artifacts = element.querySelectorAll(".ai-artifact");
8+
if (!artifacts.length) {
9+
return;
10+
}
11+
12+
artifacts.forEach((artifact) => {
13+
const tabs = artifact.querySelectorAll(".ai-artifact-tab");
14+
const panels = artifact.querySelectorAll(".ai-artifact-panel");
15+
16+
tabs.forEach((tab) => {
17+
tab.addEventListener("click", (e) => {
18+
e.preventDefault();
19+
20+
if (tab.hasAttribute("data-selected")) {
21+
return;
22+
}
23+
24+
const tabType = Object.keys(tab.dataset).find(
25+
(key) => key !== "selected"
26+
);
27+
28+
tabs.forEach((t) => t.removeAttribute("data-selected"));
29+
panels.forEach((p) => p.removeAttribute("data-selected"));
30+
31+
tab.setAttribute("data-selected", "");
32+
const targetPanel = artifact.querySelector(
33+
`.ai-artifact-panel[data-${tabType}]`
34+
);
35+
if (targetPanel) {
36+
targetPanel.setAttribute("data-selected", "");
37+
}
38+
});
39+
});
40+
});
41+
},
42+
{
43+
id: "ai-artifact-tabs",
44+
onlyStream: false,
45+
}
46+
);
47+
}
48+
49+
export default {
50+
name: "ai-artifact-tabs",
51+
initialize() {
52+
withPluginApi("0.8.7", initializeAiArtifactTabs);
53+
},
54+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
export function setup(helper) {
22
helper.allowList(["details[class=ai-quote]"]);
3+
helper.allowList(["div[class=ai-artifact]"]);
4+
helper.allowList(["div[class=ai-artifact-tab]"]);
5+
helper.allowList(["div[class=ai-artifact-tabs]"]);
6+
helper.allowList(["div[class=ai-artifact-panels]"]);
7+
helper.allowList(["div[class=ai-artifact-panel]"]);
38
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
.ai-artifact {
2+
margin: 1em 0;
3+
4+
.ai-artifact-tabs {
5+
display: flex;
6+
gap: 0.20em;
7+
border-bottom: 2px solid var(--primary-low);
8+
padding: 0 0.2em;
9+
10+
.ai-artifact-tab {
11+
margin-bottom: -2px;
12+
13+
&[data-selected] {
14+
a {
15+
color: var(--tertiary);
16+
font-weight: 500;
17+
border-bottom: 2px solid var(--tertiary);
18+
}
19+
}
20+
21+
&:hover:not([data-selected]) {
22+
a {
23+
color: var(--primary);
24+
background: var(--primary-very-low);
25+
}
26+
}
27+
28+
a {
29+
display: block;
30+
padding: 0.5em 1em;
31+
color: var(--primary-medium);
32+
text-decoration: none;
33+
cursor: pointer;
34+
border-bottom: 2px solid transparent;
35+
}
36+
}
37+
}
38+
39+
.ai-artifact-panels {
40+
padding: 1em 0 0 0;
41+
background: var(--blend-primary-secondary-5);
42+
43+
.ai-artifact-panel {
44+
display: none;
45+
min-height: 400px;
46+
47+
&[data-selected] {
48+
display: block;
49+
}
50+
51+
pre {
52+
margin: 0;
53+
}
54+
}
55+
}
56+
}

config/locales/server.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ en:
220220
name: "Base Search Query"
221221
description: "Base query to use when searching. Example: '#urgent' will prepend '#urgent' to the search query and only include topics with the urgent category or tag."
222222
tool_summary:
223+
create_artifact: "Create web artifact"
223224
web_browser: "Browse Web"
224225
github_search_files: "GitHub search files"
225226
github_search_code: "GitHub code search"
@@ -241,6 +242,7 @@ en:
241242
search_meta_discourse: "Search Meta Discourse"
242243
javascript_evaluator: "Evaluate JavaScript"
243244
tool_help:
245+
create_artifact: "Create a web artifact using the AI Bot"
244246
web_browser: "Browse web page using the AI Bot"
245247
github_search_code: "Search for code in a GitHub repository"
246248
github_search_files: "Search for files in a GitHub repository"
@@ -262,6 +264,7 @@ en:
262264
search_meta_discourse: "Search Meta Discourse"
263265
javascript_evaluator: "Evaluate JavaScript"
264266
tool_description:
267+
create_artifact: "Created a web artifact using the AI Bot"
265268
web_browser: "Reading <a href='%{url}'>%{url}</a>"
266269
github_search_files: "Searched for '%{keywords}' in %{repo}/%{branch}"
267270
github_search_code: "Searched for '%{query}' in %{repo}"

config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
get "/preview/:topic_id" => "shared_ai_conversations#preview"
3333
end
3434

35+
scope module: :ai_bot, path: "/ai-bot/artifacts" do
36+
get "/:id" => "artifacts#show"
37+
end
38+
3539
scope module: :summarization, path: "/summarization", defaults: { format: :json } do
3640
get "/t/:topic_id" => "summary#show", :constraints => { topic_id: /\d+/ }
3741
get "/channels/:channel_id" => "chat_summary#show"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
class AddAiArtifacts < ActiveRecord::Migration[7.1]
3+
def change
4+
create_table :ai_artifacts do |t|
5+
t.integer :user_id, null: false
6+
t.integer :post_id, null: false
7+
t.string :name, null: false, limit: 255
8+
t.string :html, limit: 65535 # ~64KB limit
9+
t.string :css, limit: 65535 # ~64KB limit
10+
t.string :js, limit: 65535 # ~64KB limit
11+
t.jsonb :metadata # For any additional properties
12+
13+
t.timestamps
14+
end
15+
end
16+
end

lib/ai_bot/personas/persona.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def all_available_tools
9696
Tools::GithubSearchFiles,
9797
Tools::WebBrowser,
9898
Tools::JavascriptEvaluator,
99+
Tools::CreateArtifact,
99100
]
100101

101102
tools << Tools::GithubSearchCode if SiteSetting.ai_bot_github_access_token.present?

0 commit comments

Comments
 (0)