don't blow up metadata when sharing an artifact
This commit is contained in:
parent
6e4222c1c6
commit
75e667423f
|
@ -38,7 +38,11 @@ class AiArtifact < ActiveRecord::Base
|
||||||
|
|
||||||
def self.share_publicly(id:, post:)
|
def self.share_publicly(id:, post:)
|
||||||
artifact = AiArtifact.find_by(id: id)
|
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
|
end
|
||||||
|
|
||||||
def self.unshare_publicly(id:)
|
def self.unshare_publicly(id:)
|
||||||
|
|
|
@ -37,7 +37,9 @@ class SharedAiConversation < ActiveRecord::Base
|
||||||
|
|
||||||
maybe_topic = conversation.target
|
maybe_topic = conversation.target
|
||||||
if maybe_topic.is_a?(Topic)
|
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
|
end
|
||||||
|
|
||||||
::Jobs.enqueue(
|
::Jobs.enqueue(
|
||||||
|
|
|
@ -64,6 +64,13 @@ module DiscourseAi
|
||||||
description: specification_description,
|
description: specification_description,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "requires_storage",
|
||||||
|
description:
|
||||||
|
"Does the artifact require storage for data? (e.g., user input, settings)",
|
||||||
|
type: "boolean",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -223,6 +230,7 @@ module DiscourseAi
|
||||||
js: code[:js],
|
js: code[:js],
|
||||||
metadata: {
|
metadata: {
|
||||||
specification: parameters[:specification],
|
specification: parameters[:specification],
|
||||||
|
requires_storage: !!parameters[:requires_storage],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -265,9 +273,62 @@ module DiscourseAi
|
||||||
- Include basic error handling
|
- Include basic error handling
|
||||||
- Follow accessibility guidelines
|
- Follow accessibility guidelines
|
||||||
- No explanatory text, only code
|
- No explanatory text, only code
|
||||||
|
|
||||||
|
#{storage_api}
|
||||||
PROMPT
|
PROMPT
|
||||||
end
|
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)
|
def update_custom_html(artifact)
|
||||||
html_preview = <<~MD
|
html_preview = <<~MD
|
||||||
[details="View Source"]
|
[details="View Source"]
|
||||||
|
|
|
@ -20,7 +20,8 @@ module DiscourseAi
|
||||||
- Focus on visual appeal and smooth animations
|
- Focus on visual appeal and smooth animations
|
||||||
- Write clean, efficient code
|
- Write clean, efficient code
|
||||||
- Build progressively (HTML structure → CSS styling → JavaScript interactivity)
|
- 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:
|
When creating:
|
||||||
1. Understand the desired user experience
|
1. Understand the desired user experience
|
||||||
|
|
|
@ -74,6 +74,44 @@ RSpec.describe SharedAiConversation, type: :model do
|
||||||
expect(populated_context[1].user.id).to eq(post2.user.id)
|
expect(populated_context[1].user.id).to eq(post2.user.id)
|
||||||
end
|
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
|
it "escapes HTML" do
|
||||||
conversation = described_class.share_conversation(user, topic)
|
conversation = described_class.share_conversation(user, topic)
|
||||||
onebox = conversation.onebox
|
onebox = conversation.onebox
|
||||||
|
|
Loading…
Reference in New Issue