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

Commit 41a002b

Browse files
committed
FEATURE: AI artifacts
Initial implementation of an artifact system which allows users to generate HTML pages directly from the AI persona. FEATURE: support tool progress callbacks This is anthropic only for now, but we can get a callback as tool is completing, this gives us the ability to show progress to user as the function is populating. work in progress Revert "work in progress" This reverts commit 30ebe56. Revert "FEATURE: support tool progress callbacks" This reverts commit fd7ccfd.
1 parent 823e8ef commit 41a002b

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
@@ -222,6 +222,7 @@ en:
222222
name: "Base Search Query"
223223
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."
224224
tool_summary:
225+
create_artifact: "Create web artifact"
225226
web_browser: "Browse Web"
226227
github_search_files: "GitHub search files"
227228
github_search_code: "GitHub code search"
@@ -243,6 +244,7 @@ en:
243244
search_meta_discourse: "Search Meta Discourse"
244245
javascript_evaluator: "Evaluate JavaScript"
245246
tool_help:
247+
create_artifact: "Create a web artifact using the AI Bot"
246248
web_browser: "Browse web page using the AI Bot"
247249
github_search_code: "Search for code in a GitHub repository"
248250
github_search_files: "Search for files in a GitHub repository"
@@ -264,6 +266,7 @@ en:
264266
search_meta_discourse: "Search Meta Discourse"
265267
javascript_evaluator: "Evaluate JavaScript"
266268
tool_description:
269+
create_artifact: "Created a web artifact using the AI Bot"
267270
web_browser: "Reading <a href='%{url}'>%{url}</a>"
268271
github_search_files: "Searched for '%{keywords}' in %{repo}/%{branch}"
269272
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
@@ -33,6 +33,10 @@
3333
get "/preview/:topic_id" => "shared_ai_conversations#preview"
3434
end
3535

36+
scope module: :ai_bot, path: "/ai-bot/artifacts" do
37+
get "/:id" => "artifacts#show"
38+
end
39+
3640
scope module: :summarization, path: "/summarization", defaults: { format: :json } do
3741
get "/t/:topic_id" => "summary#show", :constraints => { topic_id: /\d+/ }
3842
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)