don't blow up metadata when sharing an artifact

This commit is contained in:
Sam Saffron 2025-06-09 12:06:42 +10:00
parent 6e4222c1c6
commit 75e667423f
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
5 changed files with 109 additions and 3 deletions

View File

@ -38,7 +38,11 @@ class AiArtifact < ActiveRecord::Base
def self.share_publicly(id:, post:)
artifact = AiArtifact.find_by(id: id)
artifact.update!(metadata: { public: true }) if artifact&.post&.topic&.id == post.topic.id
if artifact&.post&.topic&.id == post.topic.id
artifact.metadata ||= {}
artifact.metadata[:public] = true
artifact.save!
end
end
def self.unshare_publicly(id:)

View File

@ -37,7 +37,9 @@ class SharedAiConversation < ActiveRecord::Base
maybe_topic = conversation.target
if maybe_topic.is_a?(Topic)
AiArtifact.where(post: maybe_topic.posts).update_all(metadata: { public: false })
AiArtifact.where(post: maybe_topic.posts).update_all(
"metadata = jsonb_set(COALESCE(metadata, '{}'), '{public}', 'false')",
)
end
::Jobs.enqueue(

View File

@ -64,6 +64,13 @@ module DiscourseAi
description: specification_description,
required: true,
},
{
name: "requires_storage",
description:
"Does the artifact require storage for data? (e.g., user input, settings)",
type: "boolean",
required: true,
},
],
}
end
@ -223,6 +230,7 @@ module DiscourseAi
js: code[:js],
metadata: {
specification: parameters[:specification],
requires_storage: !!parameters[:requires_storage],
},
)
end
@ -265,9 +273,62 @@ module DiscourseAi
- Include basic error handling
- Follow accessibility guidelines
- No explanatory text, only code
#{storage_api}
PROMPT
end
def storage_api
return if !parameters[:requires_storage]
self.class.storage_api
end
def self.storage_api
<<~API
## Storage API
Your artifact has access to a persistent key-value storage system via `window.discourseArtifact`:
### Methods Available:
**get(key)**
- Parameters: key (string) - The key to retrieve
- Returns: Promise<string|null> - The stored value or null if not found
- Example: `const value = await window.discourseArtifact.get('user_name');`
**set(key, value, options)**
- Parameters:
- key (string) - The key to store (max 50 characters)
- value (string) - The value to store (max 5000 characters)
- options (object, optional) - { public: boolean } - Whether other users can read this value
- Returns: Promise<object> - The created/updated key-value record
- Example: `await window.discourseArtifact.set('score', '100', { public: true });`
**delete(key)**
- Parameters: key (string) - The key to delete
- Returns: Promise<boolean> - true if successful
- Example: `await window.discourseArtifact.delete('temp_data');`
**index(filter)**
- Parameters: filter (object, optional) - Filtering options:
- key (string) - Filter by specific key
- all_users (boolean) - Include other users' public values
- keys_only (boolean) - Return only keys, not values
- page (number) - Page number for pagination
- per_page (number) - Items per page (max 100, default 100)
- Returns: Promise<object> - { key_values: Array(key, value), has_more: boolean, total_count: number }
- Example: `const result = await window.discourseArtifact.index({ keys_only: true });`
### Storage Rules:
- Each user can store up to 100 keys per artifact
- Keys are scoped to the current user and artifact
- Private values are only accessible to the user who created them
- Public values can be read by anyone who can view the artifact
- All operations are asynchronous and return Promises
```
API
end
def update_custom_html(artifact)
html_preview = <<~MD
[details="View Source"]

View File

@ -20,7 +20,8 @@ module DiscourseAi
- Focus on visual appeal and smooth animations
- Write clean, efficient code
- Build progressively (HTML structure CSS styling JavaScript interactivity)
- Keep components focused and purposeful
- Artifacts run in a sandboxed IFRAME environmment
- Artifacts optionally have support for a Discourse user persistent storage
When creating:
1. Understand the desired user experience

View File

@ -74,6 +74,44 @@ RSpec.describe SharedAiConversation, type: :model do
expect(populated_context[1].user.id).to eq(post2.user.id)
end
it "shares artifacts publicly when conversation is shared" do
# Create a post with an AI artifact
artifact =
Fabricate(
:ai_artifact,
post: post1,
user: user,
metadata: {
public: false,
something: "good",
},
)
_post_with_artifact =
Fabricate(
:post,
topic: topic,
post_number: 3,
raw: "Here's an artifact",
cooked:
"<div class='ai-artifact' data-ai-artifact-id='#{artifact.id}' data-ai-artifact-version='1'></div>",
)
expect(artifact.public?).to be_falsey
conversation = described_class.share_conversation(user, topic)
artifact.reload
expect(artifact.metadata["something"]).to eq("good")
expect(artifact.public?).to be_truthy
described_class.destroy_conversation(conversation)
artifact.reload
expect(artifact.metadata["something"]).to eq("good")
expect(artifact.public?).to be_falsey
end
it "escapes HTML" do
conversation = described_class.share_conversation(user, topic)
onebox = conversation.onebox