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

Commit 581a25d

Browse files
committed
don't blow up metadata when sharing an artifact
1 parent 453ff2d commit 581a25d

File tree

5 files changed

+109
-3
lines changed

5 files changed

+109
-3
lines changed

app/models/ai_artifact.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ def self.url(id, version = nil)
3838

3939
def self.share_publicly(id:, post:)
4040
artifact = AiArtifact.find_by(id: id)
41-
artifact.update!(metadata: { public: true }) if artifact&.post&.topic&.id == post.topic.id
41+
if artifact&.post&.topic&.id == post.topic.id
42+
artifact.metadata ||= {}
43+
artifact.metadata[:public] = true
44+
artifact.save!
45+
end
4246
end
4347

4448
def self.unshare_publicly(id:)

app/models/shared_ai_conversation.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def self.destroy_conversation(conversation)
3737

3838
maybe_topic = conversation.target
3939
if maybe_topic.is_a?(Topic)
40-
AiArtifact.where(post: maybe_topic.posts).update_all(metadata: { public: false })
40+
AiArtifact.where(post: maybe_topic.posts).update_all(
41+
"metadata = jsonb_set(COALESCE(metadata, '{}'), '{public}', 'false')",
42+
)
4143
end
4244

4345
::Jobs.enqueue(

lib/personas/tools/create_artifact.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ def self.signature
6464
description: specification_description,
6565
required: true,
6666
},
67+
{
68+
name: "requires_storage",
69+
description:
70+
"Does the artifact require storage for data? (e.g., user input, settings)",
71+
type: "boolean",
72+
required: true,
73+
},
6774
],
6875
}
6976
end
@@ -223,6 +230,7 @@ def create_artifact(post, code)
223230
js: code[:js],
224231
metadata: {
225232
specification: parameters[:specification],
233+
requires_storage: !!parameters[:requires_storage],
226234
},
227235
)
228236
end
@@ -265,9 +273,62 @@ def artifact_system_prompt
265273
- Include basic error handling
266274
- Follow accessibility guidelines
267275
- No explanatory text, only code
276+
277+
#{storage_api}
268278
PROMPT
269279
end
270280

281+
def storage_api
282+
return if !parameters[:requires_storage]
283+
self.class.storage_api
284+
end
285+
286+
def self.storage_api
287+
<<~API
288+
## Storage API
289+
290+
Your artifact has access to a persistent key-value storage system via `window.discourseArtifact`:
291+
292+
### Methods Available:
293+
294+
**get(key)**
295+
- Parameters: key (string) - The key to retrieve
296+
- Returns: Promise<string|null> - The stored value or null if not found
297+
- Example: `const value = await window.discourseArtifact.get('user_name');`
298+
299+
**set(key, value, options)**
300+
- Parameters:
301+
- key (string) - The key to store (max 50 characters)
302+
- value (string) - The value to store (max 5000 characters)
303+
- options (object, optional) - { public: boolean } - Whether other users can read this value
304+
- Returns: Promise<object> - The created/updated key-value record
305+
- Example: `await window.discourseArtifact.set('score', '100', { public: true });`
306+
307+
**delete(key)**
308+
- Parameters: key (string) - The key to delete
309+
- Returns: Promise<boolean> - true if successful
310+
- Example: `await window.discourseArtifact.delete('temp_data');`
311+
312+
**index(filter)**
313+
- Parameters: filter (object, optional) - Filtering options:
314+
- key (string) - Filter by specific key
315+
- all_users (boolean) - Include other users' public values
316+
- keys_only (boolean) - Return only keys, not values
317+
- page (number) - Page number for pagination
318+
- per_page (number) - Items per page (max 100, default 100)
319+
- Returns: Promise<object> - { key_values: Array(key, value), has_more: boolean, total_count: number }
320+
- Example: `const result = await window.discourseArtifact.index({ keys_only: true });`
321+
322+
### Storage Rules:
323+
- Each user can store up to 100 keys per artifact
324+
- Keys are scoped to the current user and artifact
325+
- Private values are only accessible to the user who created them
326+
- Public values can be read by anyone who can view the artifact
327+
- All operations are asynchronous and return Promises
328+
```
329+
API
330+
end
331+
271332
def update_custom_html(artifact)
272333
html_preview = <<~MD
273334
[details="View Source"]

lib/personas/web_artifact_creator.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def system_prompt
2020
- Focus on visual appeal and smooth animations
2121
- Write clean, efficient code
2222
- Build progressively (HTML structure → CSS styling → JavaScript interactivity)
23-
- Keep components focused and purposeful
23+
- Artifacts run in a sandboxed IFRAME environmment
24+
- Artifacts optionally have support for a Discourse user persistent storage
2425
2526
When creating:
2627
1. Understand the desired user experience

spec/models/shared_ai_conversation_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,44 @@
7474
expect(populated_context[1].user.id).to eq(post2.user.id)
7575
end
7676

77+
it "shares artifacts publicly when conversation is shared" do
78+
# Create a post with an AI artifact
79+
artifact =
80+
Fabricate(
81+
:ai_artifact,
82+
post: post1,
83+
user: user,
84+
metadata: {
85+
public: false,
86+
something: "good",
87+
},
88+
)
89+
90+
_post_with_artifact =
91+
Fabricate(
92+
:post,
93+
topic: topic,
94+
post_number: 3,
95+
raw: "Here's an artifact",
96+
cooked:
97+
"<div class='ai-artifact' data-ai-artifact-id='#{artifact.id}' data-ai-artifact-version='1'></div>",
98+
)
99+
100+
expect(artifact.public?).to be_falsey
101+
102+
conversation = described_class.share_conversation(user, topic)
103+
artifact.reload
104+
105+
expect(artifact.metadata["something"]).to eq("good")
106+
expect(artifact.public?).to be_truthy
107+
108+
described_class.destroy_conversation(conversation)
109+
artifact.reload
110+
111+
expect(artifact.metadata["something"]).to eq("good")
112+
expect(artifact.public?).to be_falsey
113+
end
114+
77115
it "escapes HTML" do
78116
conversation = described_class.share_conversation(user, topic)
79117
onebox = conversation.onebox

0 commit comments

Comments
 (0)