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

Commit c44c700

Browse files
committed
work in progress, but basic diffing is working
1 parent a82e012 commit c44c700

File tree

7 files changed

+699
-444
lines changed

7 files changed

+699
-444
lines changed

lib/ai_bot/tools/update_artifact.rb

Lines changed: 145 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -10,52 +10,39 @@ def self.name
1010

1111
def self.diff_examples
1212
<<~EXAMPLES
13-
Example 1 - Adding a new button:
14-
Original HTML:
15-
<div class="calculator">
16-
<div class="display">0</div>
17-
<button>1</button>
18-
</div>
19-
20-
Diff to add a new button:
21-
<button>1</button>
22-
+ <button>2</button>
23-
24-
Example 2 - Modifying styles:
25-
Original CSS:
26-
.button {
27-
background: blue;
28-
color: white;
29-
}
30-
31-
Diff to change colors:
32-
.button {
33-
- background: blue;
34-
- color: white;
35-
+ background: #333;
36-
+ color: #fff;
37-
38-
Example 3 - Updating JavaScript:
39-
Original JavaScript:
40-
function ignore() {
41-
// some function that is not part of diff
42-
}
43-
function calculate() {
44-
return a + b;
45-
}
46-
47-
Diff to add multiplication:
48-
function calculate() {
49-
- return a + b;
50-
+ return operation === 'multiply' ? a * b : a + b;
51-
}
13+
Example - Multiple changes in one file:
14+
--- JavaScript ---
15+
<<<<<<< SEARCH
16+
console.log('old1');
17+
=======
18+
console.log('new1');
19+
>>>>>>> REPLACE
20+
<<<<<<< SEARCH
21+
console.log('old2');
22+
=======
23+
console.log('new2');
24+
>>>>>>> REPLACE
25+
26+
Example - CSS with multiple blocks:
27+
--- CSS ---
28+
<<<<<<< SEARCH
29+
.button { color: blue; }
30+
=======
31+
.button { color: red; }
32+
>>>>>>> REPLACE
33+
<<<<<<< SEARCH
34+
.text { font-size: 12px; }
35+
=======
36+
.text { font-size: 16px; }
37+
>>>>>>> REPLACE
5238
EXAMPLES
5339
end
5440

5541
def self.signature
5642
{
5743
name: "update_artifact",
58-
description: "Updates an existing web artifact by generating precise diffs",
44+
description:
45+
"Updates an existing web artifact using search/replace operations. Supports multiple changes per section.",
5946
parameters: [
6047
{
6148
name: "artifact_id",
@@ -86,160 +73,172 @@ def invoke
8673
return error_response("Attempting to update an artifact you are not allowed to")
8774
end
8875

89-
diffs = generate_diffs(post: post, user: post.user, artifact: artifact)
90-
return error_response(diffs[:error]) if diffs[:error]
91-
92-
p "here"
76+
changes = generate_changes(post: post, user: post.user, artifact: artifact)
77+
return error_response(changes[:error]) if changes[:error]
9378

9479
begin
95-
version =
96-
artifact.apply_diff(
97-
html_diff: diffs[:html_diff],
98-
css_diff: diffs[:css_diff],
99-
js_diff: diffs[:js_diff],
100-
change_description: parameters[:instructions],
101-
)
102-
103-
p "good"
80+
version = apply_changes(artifact, changes)
10481
update_custom_html(artifact, version)
10582
success_response(artifact, version)
106-
rescue DiscourseAi::Utils::DiffUtils::DiffError => e
107-
p e
108-
error_response(e.to_llm_message)
10983
rescue => e
110-
p e
11184
error_response(e.message)
11285
end
11386
end
11487

11588
private
11689

117-
def generate_diffs(post:, user:, artifact:)
118-
prompt = build_diff_prompt(post: post, artifact: artifact)
90+
def generate_changes(post:, user:, artifact:)
91+
prompt = build_changes_prompt(post: post, artifact: artifact)
11992
response = +""
12093

121-
llm.generate(prompt, user: user, feature_name: "update_artifact") do |partial_response|
122-
response << partial_response
94+
llm.generate(prompt, user: user, feature_name: "update_artifact") do |partial|
95+
response << partial
96+
end
97+
98+
parse_changes(response)
99+
end
100+
101+
def parse_changes(response)
102+
sections = { html: nil, css: nil, javascript: nil }
103+
current_section = nil
104+
lines = []
105+
106+
response.each_line do |line|
107+
case line
108+
when /^--- (HTML|CSS|JavaScript) ---$/
109+
sections[current_section] = lines.join if current_section && !lines.empty?
110+
current_section = line.match(/^--- (.+) ---$/)[1].downcase.to_sym
111+
lines = []
112+
else
113+
lines << line if current_section
114+
end
115+
end
116+
117+
sections[current_section] = lines.join if current_section && !lines.empty?
118+
119+
# Validate and extract all search/replace blocks
120+
sections.transform_values do |content|
121+
next nil if content.nil?
122+
123+
puts content
124+
125+
blocks = extract_search_replace_blocks(content)
126+
return { error: "Invalid format in #{current_section} section" } if blocks.nil?
127+
128+
puts "GOOD"
129+
blocks
130+
end
131+
end
132+
133+
def extract_search_replace_blocks(content)
134+
return nil if content.blank?
135+
136+
blocks = []
137+
remaining = content
138+
139+
while remaining =~ /<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE/m
140+
blocks << { search: $1, replace: $2 }
141+
remaining = $'
123142
end
124143

125-
sections = parse_diff_sections(response)
126-
127-
if valid_diff_sections?(sections)
128-
html_diff, css_diff, js_diff = sections
129-
{
130-
html_diff: html_diff.presence,
131-
css_diff: css_diff.presence,
132-
js_diff: js_diff.presence,
133-
}
134-
else
135-
{ error: "Failed to generate valid diffs", response: response }
144+
blocks.empty? ? nil : blocks
145+
end
146+
147+
def apply_changes(artifact, changes)
148+
updated_content = {}
149+
150+
%i[html css javascript].each do |section|
151+
blocks = changes[section]
152+
next unless blocks
153+
154+
content = artifact.send(section == :javascript ? :js : section)
155+
blocks.each do |block|
156+
content =
157+
DiscourseAi::Utils::DiffUtils::SimpleDiff.apply(
158+
content,
159+
block[:search],
160+
block[:replace],
161+
)
162+
end
163+
updated_content[section == :javascript ? :js : section] = content
136164
end
165+
166+
artifact.create_new_version(
167+
html: updated_content[:html],
168+
css: updated_content[:css],
169+
js: updated_content[:js],
170+
change_description: parameters[:instructions],
171+
)
137172
end
138173

139-
def build_diff_prompt(post:, artifact:)
174+
def build_changes_prompt(post:, artifact:)
140175
DiscourseAi::Completions::Prompt.new(
141-
diff_system_prompt,
176+
changes_system_prompt,
142177
messages: [
143-
{
144-
type: :user,
145-
content:
146-
"Current artifact code:\n\n" \
147-
"--- HTML ---\n#{artifact.html}\n" \
148-
"--- CSS ---\n#{artifact.css}\n" \
149-
"--- JavaScript ---\n#{artifact.js}\n",
150-
},
151-
{ type: :model, content: "Please explain the diffs you would like to generate:" },
178+
{ type: :user, content: <<~CONTENT },
179+
Current artifact code:
180+
181+
--- HTML ---
182+
#{artifact.html}
183+
184+
--- CSS ---
185+
#{artifact.css}
186+
187+
--- JavaScript ---
188+
#{artifact.js}
189+
CONTENT
190+
{ type: :model, content: "Please explain the changes you would like to generate:" },
152191
{ type: :user, content: parameters[:instructions] },
153192
],
154193
post_id: post.id,
155194
topic_id: post.topic_id,
156195
)
157196
end
158197

159-
def diff_system_prompt
198+
def changes_system_prompt
160199
<<~PROMPT
161-
You are a web development expert generating precise diffs for updating HTML, CSS, and JavaScript code.
200+
You are a web development expert generating precise search/replace changes for updating HTML, CSS, and JavaScript code.
162201
163202
Important rules:
164-
1. Only output changes using - for removals and + for additions
165-
2. Include 1-2 lines of context around changes
166-
3. Generate three sections: HTML_DIFF, CSS_DIFF, and JS_DIFF
203+
1. Use the format <<<<<<< SEARCH / ======= / >>>>>>> REPLACE for each change
204+
2. You can specify multiple search/replace blocks per section
205+
3. Generate three sections: HTML, CSS, and JavaScript
167206
4. Only include sections that have changes
168-
5. Use exact line matches for context
169-
6. Keep diffs minimal and focused
207+
5. Keep changes minimal and focused
208+
6. Use exact matches for the search content
170209
171210
Format:
172-
--- HTML_DIFF ---
173-
(diff or empty if no changes)
174-
--- CSS_DIFF ---
175-
(diff or empty if no changes)
176-
--- JS_DIFF ---
177-
(diff or empty if no changes)
178-
179-
--------------
180-
When supplying diffs, use a unified diff format. For example:
181-
211+
--- HTML ---
212+
(changes or empty if no changes)
213+
--- CSS ---
214+
(changes or empty if no changes)
215+
--- JavaScript ---
216+
(changes or empty if no changes)
217+
218+
Example changes:
182219
#{self.class.diff_examples}
183220
PROMPT
184221
end
185222

186-
def parse_diff_sections(response)
187-
html = +""
188-
css = +""
189-
javascript = +""
190-
current_section = nil
191-
192-
response.each_line do |line|
193-
case line
194-
when /--- (HTML_DIFF|CSS_DIFF|JS_DIFF) ---/
195-
current_section = Regexp.last_match(1)
196-
else
197-
case current_section
198-
when "HTML_DIFF"
199-
html << line
200-
when "CSS_DIFF"
201-
css << line
202-
when "JS_DIFF"
203-
javascript << line
204-
end
205-
end
206-
end
207-
208-
[html.strip, css.strip, javascript.strip]
209-
end
210-
211-
def valid_diff_sections?(sections)
212-
return false if sections.empty?
213-
214-
sections.any? do |section|
215-
next true if section.blank?
216-
section.include?("-") || section.include?("+")
217-
end
218-
end
219-
220223
def update_custom_html(artifact, version)
221224
content = []
222225

223226
if version.change_description.present?
224227
content << [:description, "### Change Description\n\n#{version.change_description}"]
225228
end
226-
227-
diffs = []
228-
diffs << ["HTML Changes", version.html] if version.html != artifact.html
229-
diffs << ["CSS Changes", version.css] if version.css != artifact.css
230-
diffs << ["JavaScript Changes", version.js] if version.js != artifact.js
231-
232229
content << [nil, "[details='#{I18n.t("discourse_ai.ai_artifact.view_changes")}']"]
233230

234-
diffs.each do |title, new_content|
235-
old_content = artifact.send(title.downcase.split.first)
236-
#diff = generate_readable_diff(old_content, new_content)
237-
diff = "TODO"
238-
content << [nil, "### #{title}\n```diff\n#{diff}\n```"]
231+
%w[html css js].each do |type|
232+
old_content = artifact.public_send(type)
233+
new_content = version.public_send(type)
234+
235+
if old_content != new_content
236+
diff = "xxx" # Placeholder for actual diff implementation
237+
content << [nil, "### #{type.upcase} Changes\n```diff\n#{diff}\n```"]
238+
end
239239
end
240240

241241
content << [nil, "[/details]"]
242-
243242
content << [
244243
:preview,
245244
"### Preview\n\n<div class=\"ai-artifact\" data-ai-artifact-version=\"#{version.version_number}\" data-ai-artifact-id=\"#{artifact.id}\"></div>",
@@ -248,10 +247,6 @@ def update_custom_html(artifact, version)
248247
self.custom_raw = content.map { |c| c[1] }.join("\n\n")
249248
end
250249

251-
def generate_readable_diff(old_content, new_content)
252-
#Diffy::Diff.new(old_content, new_content, context: 2).to_s(:text)
253-
end
254-
255250
def success_response(artifact, version)
256251
{
257252
status: "success",

0 commit comments

Comments
 (0)