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

Commit b976d5a

Browse files
committed
work in progress artifact system revamp
1 parent c44c700 commit b976d5a

File tree

7 files changed

+426
-177
lines changed

7 files changed

+426
-177
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
module DiscourseAi
3+
module AiBot
4+
module ArtifactUpdateStrategies
5+
class InvalidFormatError < StandardError
6+
end
7+
class Base
8+
attr_reader :post, :user, :artifact, :artifact_version, :instructions, :llm
9+
10+
def initialize(llm:, post:, user:, artifact:, artifact_version:, instructions:)
11+
@llm = llm
12+
@post = post
13+
@user = user
14+
@artifact = artifact
15+
@artifact_version = artifact_version
16+
@instructions = instructions
17+
end
18+
19+
def apply
20+
changes = generate_changes
21+
parsed_changes = parse_changes(changes)
22+
apply_changes(parsed_changes)
23+
end
24+
25+
private
26+
27+
def generate_changes
28+
response = +""
29+
llm.generate(build_prompt, user: user) { |partial| response << partial }
30+
end
31+
32+
def build_prompt
33+
# To be implemented by subclasses
34+
raise NotImplementedError
35+
end
36+
37+
def parse_changes(response)
38+
# To be implemented by subclasses
39+
raise NotImplementedError
40+
end
41+
42+
def apply_changes(changes)
43+
# To be implemented by subclasses
44+
raise NotImplementedError
45+
end
46+
end
47+
end
48+
end
49+
end
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# frozen_string_literal: true
2+
module DiscourseAi
3+
module AiBot
4+
module ArtifactUpdateStrategies
5+
class Diff < Base
6+
private
7+
8+
def build_prompt
9+
DiscourseAi::Completions::Prompt.new(
10+
system_prompt,
11+
messages: [
12+
{ type: :user, content: current_artifact_content },
13+
{ type: :model, content: "Please explain the changes you would like to generate:" },
14+
{ type: :user, content: instructions },
15+
],
16+
post_id: post.id,
17+
topic_id: post.topic_id,
18+
)
19+
end
20+
21+
def parse_changes(response)
22+
sections = { html: nil, css: nil, javascript: nil }
23+
current_section = nil
24+
lines = []
25+
26+
response.each_line do |line|
27+
case line
28+
when /^--- (HTML|CSS|JavaScript) ---$/
29+
sections[current_section] = lines.join if current_section && !lines.empty?
30+
current_section = line.match(/^--- (.+) ---$/)[1].downcase.to_sym
31+
lines = []
32+
else
33+
lines << line if current_section
34+
end
35+
end
36+
37+
sections[current_section] = lines.join if current_section && !lines.empty?
38+
39+
sections.transform_values do |content|
40+
next nil if content.nil?
41+
blocks = extract_search_replace_blocks(content)
42+
raise InvalidFormatError, "Invalid format in #{current_section} section" if blocks.nil?
43+
blocks
44+
end
45+
end
46+
47+
def apply_changes(changes)
48+
source = artifact_version || artifact
49+
updated_content = { js: source.js, html: source.html, css: source.css }
50+
51+
%i[html css javascript].each do |section|
52+
blocks = changes[section]
53+
next unless blocks
54+
55+
content = source.public_send(section == :javascript ? :js : section)
56+
blocks.each do |block|
57+
content =
58+
DiscourseAi::Utils::DiffUtils::SimpleDiff.apply(
59+
content,
60+
block[:search],
61+
block[:replace],
62+
)
63+
end
64+
updated_content[section == :javascript ? :js : section] = content
65+
end
66+
67+
artifact.create_new_version(
68+
html: updated_content[:html],
69+
css: updated_content[:css],
70+
js: updated_content[:js],
71+
change_description: instructions,
72+
)
73+
end
74+
75+
private
76+
77+
def extract_search_replace_blocks(content)
78+
return nil if content.blank?
79+
80+
blocks = []
81+
remaining = content
82+
83+
while remaining =~ /<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE/m
84+
blocks << { search: $1, replace: $2 }
85+
remaining = $'
86+
end
87+
88+
blocks.empty? ? nil : blocks
89+
end
90+
91+
def system_prompt
92+
<<~PROMPT
93+
You are a web development expert generating precise search/replace changes for updating HTML, CSS, and JavaScript code.
94+
95+
Important rules:
96+
1. Use the format <<<<<<< SEARCH / ======= / >>>>>>> REPLACE for each change
97+
2. You can specify multiple search/replace blocks per section
98+
3. Generate three sections: HTML, CSS, and JavaScript
99+
4. Only include sections that have changes
100+
5. Keep changes minimal and focused
101+
6. Use exact matches for the search content
102+
103+
Format:
104+
--- HTML ---
105+
(changes or empty if no changes)
106+
--- CSS ---
107+
(changes or empty if no changes)
108+
--- JavaScript ---
109+
(changes or empty if no changes)
110+
111+
112+
Example - Multiple changes in one file:
113+
--- JavaScript ---
114+
<<<<<<< SEARCH
115+
console.log('old1');
116+
=======
117+
console.log('new1');
118+
>>>>>>> REPLACE
119+
<<<<<<< SEARCH
120+
console.log('old2');
121+
=======
122+
console.log('new2');
123+
>>>>>>> REPLACE
124+
125+
Example - CSS with multiple blocks:
126+
--- CSS ---
127+
<<<<<<< SEARCH
128+
.button { color: blue; }
129+
=======
130+
.button { color: red; }
131+
>>>>>>> REPLACE
132+
<<<<<<< SEARCH
133+
.text { font-size: 12px; }
134+
=======
135+
.text { font-size: 16px; }
136+
>>>>>>> REPLACE
137+
PROMPT
138+
end
139+
140+
def current_artifact_content
141+
source = artifact_version || artifact
142+
<<~CONTENT
143+
Current artifact code:
144+
145+
--- HTML ---
146+
#{source.html}
147+
148+
--- CSS ---
149+
#{source.css}
150+
151+
--- JavaScript ---
152+
#{source.js}
153+
CONTENT
154+
end
155+
end
156+
end
157+
end
158+
end
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# frozen_string_literal: true
2+
module DiscourseAi
3+
module AiBot
4+
module ArtifactUpdateStrategies
5+
class Full < Base
6+
private
7+
8+
def build_prompt
9+
DiscourseAi::Completions::Prompt.new(
10+
system_prompt,
11+
messages: [
12+
{ type: :user, content: "#{current_artifact_content}\n\n\n#{instructions}" },
13+
],
14+
post_id: post.id,
15+
topic_id: post.topic_id,
16+
)
17+
end
18+
19+
def parse_changes(response)
20+
sections = { html: nil, css: nil, javascript: nil }
21+
current_section = nil
22+
lines = []
23+
24+
response.each_line do |line|
25+
case line
26+
when /^\[(HTML|CSS|JavaScript)\]$/
27+
sections[current_section] = lines.join if current_section && !lines.empty?
28+
current_section = line.match(/^\[(.+)\]$/)[1].downcase.to_sym
29+
lines = []
30+
when %r{^\[/(HTML|CSS|JavaScript)\]$}
31+
sections[current_section] = lines.join if current_section && !lines.empty?
32+
current_section = nil
33+
lines = []
34+
else
35+
lines << line if current_section
36+
end
37+
end
38+
39+
sections
40+
end
41+
42+
def apply_changes(changes)
43+
source = artifact_version || artifact
44+
updated_content = { js: source.js, html: source.html, css: source.css }
45+
46+
%i[html css javascript].each do |section|
47+
content = changes[section]&.strip
48+
next if content.blank?
49+
updated_content[section == :javascript ? :js : section] = content
50+
end
51+
52+
artifact.create_new_version(
53+
html: updated_content[:html],
54+
css: updated_content[:css],
55+
js: updated_content[:js],
56+
change_description: instructions,
57+
)
58+
end
59+
60+
private
61+
62+
def system_prompt
63+
<<~PROMPT
64+
You are a web development expert generating updated HTML, CSS, and JavaScript code.
65+
66+
Important rules:
67+
1. Provide full source code for each changed section
68+
2. Generate up to three sections: HTML, CSS, and JavaScript
69+
3. Only include sections that need changes
70+
4. Keep changes focused on the requirements
71+
5. NEVER EVER BE LAZY, always include ALL the source code with any update you make. If you are lazy you will break the artifact.
72+
6. Do not print out any reasoning, just the changed code, you will be parsed via a program.
73+
7. Sections must start and end with exact tags: [HTML] [/HTML], [CSS] [/CSS], [JavaScript] [/JavaScript]
74+
75+
Always adhere to the format when replying:
76+
77+
[HTML]
78+
complete html code, omit if no changes
79+
[/HTML]
80+
81+
[CSS]
82+
complete css code, omit if no changes
83+
[/CSS]
84+
85+
[JavaScript]
86+
complete js code, omit if no changes
87+
[/JavaScript]
88+
89+
Examples:
90+
91+
Example 1 (HTML only change):
92+
[HTML]
93+
<div class="container">
94+
<h1>Title</h1>
95+
</div>
96+
[/HTML]
97+
98+
Example 2 (CSS and JavaScript changes):
99+
[CSS]
100+
.container { padding: 20px; }
101+
.title { color: blue; }
102+
[/CSS]
103+
[JavaScript]
104+
function init() {
105+
console.log("loaded");
106+
}
107+
[/JavaScript]
108+
109+
Example 3 (All sections):
110+
[HTML]
111+
<div id="app"></div>
112+
[/HTML]
113+
[CSS]
114+
#app { margin: 0; }
115+
[/CSS]
116+
[JavaScript]
117+
const app = document.getElementById("app");
118+
[/JavaScript]
119+
120+
PROMPT
121+
end
122+
123+
def current_artifact_content
124+
source = artifact_version || artifact
125+
<<~CONTENT
126+
Current artifact code:
127+
128+
[HTML]
129+
#{source.html}
130+
[/HTML]
131+
132+
[CSS]
133+
#{source.css}
134+
[/CSS]
135+
136+
[JavaScript]
137+
#{source.js}
138+
[/JavaScript]
139+
CONTENT
140+
end
141+
end
142+
end
143+
end
144+
end

lib/ai_bot/tools/create_artifact.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def invoke
8585
end
8686
end
8787

88+
def chain_next_response?
89+
false
90+
end
91+
8892
def description_args
8993
{ name: parameters[:name], specification: parameters[:specification] }
9094
end

0 commit comments

Comments
 (0)