Dragble ai
Configure AI features and asset storage for the Dragble editor. Covers AI text generation, image generation, alt text, external AI backends, image upload, external S3 storage, and the Dragble managed storage mode.From its SKILL.md
npx -y skills add Dragble/dragble-skills --skill dragble-aiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
26.1 KB, ~6.3k tokens by cl100k_base, as published. Nobody here has run it
Dragble AI Features & Asset Storage
Overview
The Dragble editor provides two independently configured subsystems:
AI Features — Generate headings, body text, button labels, images, and alt text directly inside the editor. Three modes:
| Mode | Description | Requires |
|---|---|---|
dragble | Built-in AI powered by Dragble servers (paid plans) | Active subscription with AI credits |
external | Bring Your Own Key / backend — works on any plan | You provide callback functions that call your own API |
disabled | All AI features hidden from the editor UI | Nothing |
Asset Storage — Upload, browse, and manage images used in designs. Two modes:
| Mode | Description | Requires |
|---|---|---|
dragble | Managed cloud storage (Cloudflare R2) — zero config | Active subscription (default) |
external | Your own S3-compatible bucket or custom backend | You provide presign/upload/list callbacks |
Both subsystems are configured through dragble.init() and are completely independent of each other. You can use Dragble AI with external storage, or external AI with Dragble storage, or any combination.
AI Configuration
Mode Selection
Configure AI mode in dragble.init():
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
ai: {
mode: 'dragble' // 'dragble' | 'external' | 'disabled'
}
});
| Mode | UI Visible | Credits Used | Callbacks Required |
|---|---|---|---|
dragble | Yes | Yes (Dragble credits) | No |
external | Yes | No | Yes — you provide one or more feature callbacks |
disabled | No — all AI buttons/menus hidden | No | No |
Basic Dragble Mode (Built-in)
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
ai: {
mode: 'dragble'
}
});
No callbacks needed. The editor calls Dragble servers directly. AI credits are deducted per request based on token count.
Basic External Mode
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
ai: {
mode: 'external',
features: {
smartHeading: {
enabled: true,
generate: async (params) => {
const res = await fetch('https://your-api.com/ai/headings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
return res.json();
}
}
// ... other features
}
}
});
Hybrid Mode (Global Dragble + Individual External Overrides)
You can set the global mode to dragble but override specific features with external callbacks. The editor resolves the mode per-feature:
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
ai: {
mode: 'dragble', // global default
features: {
// This feature uses YOUR backend instead of Dragble AI
imageGeneration: {
enabled: true,
generate: async (params) => {
const res = await fetch('https://your-api.com/ai/images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
return res.json();
}
},
// smartHeading, smartText, etc. still use Dragble AI
}
}
});
Mode Resolution Order
The editor resolves which AI provider to use per-feature in this order:
disabled— Ifai.modeis'disabled', all AI features are hidden. Stop.feature.enabled: false— If a specific feature hasenabled: false, that feature is hidden. Skip it.- Has callback — If the feature has a
generatecallback, useexternalmode for that feature regardless of global mode. - Global mode — Fall back to the global
ai.modesetting ('dragble'or'external').
If global mode is 'external' but a feature has no generate callback, that feature is silently disabled.
AI Features
1. Smart Heading
Generate contextual headings for content sections.
Callback Signature:
generate: (params: {
context: string; // surrounding content / section context
tone?: string; // 'professional' | 'casual' | 'formal' | 'friendly' | 'urgent'
headingType?: string; // 'h1' | 'h2' | 'h3' | 'subject' | 'preheader'
industry?: string; // e.g. 'ecommerce', 'saas', 'healthcare'
count?: number; // number of suggestions (default: 5)
}) => Promise<{
headings: string[]; // array of heading suggestions
}>
External Example:
smartHeading: {
enabled: true,
generate: async ({ context, tone, headingType, industry, count }) => {
const res = await fetch('https://your-api.com/ai/headings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ context, tone, headingType, industry, count })
});
const data = await res.json();
return { headings: data.headings };
}
}
2. Smart Text
Generate body copy, paragraphs, or text blocks.
Callback Signature:
generate: (params: {
topic: string; // main topic or brief
purpose?: string; // 'marketing' | 'informational' | 'transactional' | 'welcome'
tone?: string; // 'professional' | 'casual' | 'formal' | 'friendly' | 'urgent'
length?: string; // 'short' | 'medium' | 'long'
industry?: string; // e.g. 'ecommerce', 'saas', 'healthcare'
keywords?: string[]; // SEO or brand keywords to include
}) => Promise<{
text: string; // primary generated text
alternatives?: string[]; // optional alternative versions
}>
External Example:
smartText: {
enabled: true,
generate: async ({ topic, purpose, tone, length, industry, keywords }) => {
const res = await fetch('https://your-api.com/ai/text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ topic, purpose, tone, length, industry, keywords })
});
const data = await res.json();
return { text: data.text, alternatives: data.alternatives };
}
}
3. Smart Button
Generate call-to-action button labels.
Callback Signature:
generate: (params: {
action: string; // what the button does (e.g. 'sign up', 'buy now')
context?: string; // surrounding content for relevance
urgency?: string; // 'low' | 'medium' | 'high'
style?: string; // 'formal' | 'casual' | 'playful' | 'direct'
count?: number; // number of suggestions (default: 5)
}) => Promise<{
buttons: string[]; // array of button label suggestions
}>
External Example:
smartButton: {
enabled: true,
generate: async ({ action, context, urgency, style, count }) => {
const res = await fetch('https://your-api.com/ai/buttons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ action, context, urgency, style, count })
});
const data = await res.json();
return { buttons: data.buttons };
}
}
4. Image Generation
Generate images from text prompts.
Callback Signature:
generate: (params: {
prompt: string; // text description of desired image
negativePrompt?: string; // what to avoid in the image
style?: string; // 'photorealistic' | 'illustration' | 'flat' | '3d' | 'watercolor'
aspectRatio?: string; // '1:1' | '16:9' | '4:3' | '9:16'
count?: number; // number of images to generate (default: 1)
}) => Promise<{
images: Array<{
url: string; // publicly accessible image URL
}>;
}>
External Example:
imageGeneration: {
enabled: true,
generate: async ({ prompt, negativePrompt, style, aspectRatio, count }) => {
const res = await fetch('https://your-api.com/ai/images/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ prompt, negativePrompt, style, aspectRatio, count })
});
const data = await res.json();
return { images: data.images.map(img => ({ url: img.url })) };
}
}
5. Image Search
Search stock photo libraries or your own image database.
Callback Signature:
generate: (params: {
query: string; // search query
orientation?: string; // 'landscape' | 'portrait' | 'square'
color?: string; // dominant color filter
perPage?: number; // results per page (default: 20)
page?: number; // pagination page number (default: 1)
}) => Promise<{
images: Array<{
id: string; // unique image identifier
source: string; // source name (e.g. 'unsplash', 'your-cdn')
url: string; // full-size image URL
thumbUrl: string; // thumbnail URL for preview grid
width: number; // image width in pixels
height: number; // image height in pixels
alt: string; // alt text / description
}>;
total: number; // total results available for pagination
}>
External Example:
imageSearch: {
enabled: true,
generate: async ({ query, orientation, color, perPage, page }) => {
const res = await fetch(`https://your-api.com/ai/images/search?q=${encodeURIComponent(query)}&orientation=${orientation || ''}&color=${color || ''}&per_page=${perPage || 20}&page=${page || 1}`, {
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});
const data = await res.json();
return {
images: data.results.map(img => ({
id: img.id,
source: 'your-cdn',
url: img.url,
thumbUrl: img.thumb,
width: img.width,
height: img.height,
alt: img.description || ''
})),
total: data.total
};
}
}
6. Alt Text
Generate accessible alt text for images.
Callback Signature:
generate: (params: {
imageUrl: string; // URL of the image to describe
context?: string; // surrounding content for relevance
maxLength?: number; // max characters for alt text (default: 125)
}) => Promise<{
altText: string; // concise alt text for the image
description?: string; // optional longer description
}>
External Example:
altText: {
enabled: true,
generate: async ({ imageUrl, context, maxLength }) => {
const res = await fetch('https://your-api.com/ai/alt-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ imageUrl, context, maxLength })
});
const data = await res.json();
return { altText: data.altText, description: data.description };
}
}
Complete External AI Configuration
Full init example with ALL AI features wired to an external backend:
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
ai: {
mode: 'external',
features: {
smartHeading: {
enabled: true,
generate: async ({ context, tone, headingType, industry, count }) => {
const res = await fetch('https://your-api.com/ai/headings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ context, tone, headingType, industry, count })
});
return res.json(); // { headings: string[] }
}
},
smartText: {
enabled: true,
generate: async ({ topic, purpose, tone, length, industry, keywords }) => {
const res = await fetch('https://your-api.com/ai/text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ topic, purpose, tone, length, industry, keywords })
});
return res.json(); // { text: string, alternatives?: string[] }
}
},
smartButton: {
enabled: true,
generate: async ({ action, context, urgency, style, count }) => {
const res = await fetch('https://your-api.com/ai/buttons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ action, context, urgency, style, count })
});
return res.json(); // { buttons: string[] }
}
},
imageGeneration: {
enabled: true,
generate: async ({ prompt, negativePrompt, style, aspectRatio, count }) => {
const res = await fetch('https://your-api.com/ai/images/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ prompt, negativePrompt, style, aspectRatio, count })
});
return res.json(); // { images: [{ url: string }] }
}
},
imageSearch: {
enabled: true,
generate: async ({ query, orientation, color, perPage, page }) => {
const res = await fetch(`https://your-api.com/ai/images/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ query, orientation, color, perPage, page })
});
return res.json(); // { images: [{ id, source, url, thumbUrl, width, height, alt }], total }
}
},
altText: {
enabled: true,
generate: async ({ imageUrl, context, maxLength }) => {
const res = await fetch('https://your-api.com/ai/alt-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ imageUrl, context, maxLength })
});
return res.json(); // { altText: string, description?: string }
}
}
}
}
});
AI Utility Methods
After initialization, you can query AI state:
// Check if AI is completely disabled
const disabled = dragble.isAIDisabled();
// Returns: true if ai.mode === 'disabled'
// Get the current AI mode
const mode = dragble.getAIMode();
// Returns: 'dragble' | 'external' | 'disabled'
Asset Storage
Mode Selection
Configure storage mode in dragble.init():
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
assetStorage: {
mode: 'dragble' // 'dragble' | 'external'
}
});
| Mode | Upload Target | Asset Management | Config Required |
|---|---|---|---|
dragble | Dragble managed cloud (Cloudflare R2) | Built-in asset manager | None (default) |
external | Your S3-compatible bucket or custom backend | You provide callbacks | Yes — at minimum getPresignUrl and onUpload |
Dragble Managed Storage (Default)
No configuration needed. Images are uploaded to Dragble's managed cloud and served via CDN:
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx'
// assetStorage defaults to { mode: 'dragble' }
});
External Storage Mode
For external storage, you must provide callback functions that handle presigned URL generation, upload confirmation, and optionally asset listing and folder management.
Callbacks:
| Callback | Required | Purpose |
|---|---|---|
getPresignUrl | Yes | Generate a presigned upload URL for the file |
onUpload | Yes | Confirm upload completion, return the final public URL |
onLoadAssets | Optional | List/paginate assets in the asset manager |
onDeleteAsset | Optional | Delete an asset from your storage |
onLoadFolders | Optional | List folders in the asset manager |
onCreateFolder | Optional | Create a new folder |
onDeleteFolder | Optional | Delete a folder |
onMoveAsset | Optional | Move an asset between folders |
Callback Signatures:
getPresignUrl: (params: {
fileName: string; // original file name
fileType: string; // MIME type (e.g. 'image/png')
fileSize: number; // file size in bytes
folder?: string; // target folder path
}) => Promise<{
uploadUrl: string; // presigned PUT URL
fileUrl: string; // final public URL after upload
fields?: Record<string, string>; // optional form fields for POST uploads
}>
onUpload: (params: {
fileUrl: string; // the public URL returned by getPresignUrl
fileName: string; // original file name
fileType: string; // MIME type
fileSize: number; // file size in bytes
folder?: string; // folder path
}) => Promise<{
url: string; // confirmed public URL for the uploaded asset
id?: string; // optional asset ID for management
}>
onLoadAssets: (params: {
folder?: string; // folder path to list (root if empty)
page?: number; // pagination page
perPage?: number; // items per page
search?: string; // search query
}) => Promise<{
assets: Array<{
id: string;
url: string;
thumbUrl?: string;
name: string;
fileType: string;
fileSize: number;
folder?: string;
createdAt?: string;
}>;
total: number;
}>
onDeleteAsset: (params: {
id: string; // asset ID
url: string; // asset URL
}) => Promise<{ success: boolean }>
onLoadFolders: (params: {
parentFolder?: string; // parent folder path
}) => Promise<{
folders: Array<{
id: string;
name: string;
path: string;
assetCount?: number;
}>;
}>
onCreateFolder: (params: {
name: string; // folder name
parentFolder?: string; // parent folder path
}) => Promise<{
id: string;
name: string;
path: string;
}>
onDeleteFolder: (params: {
id: string; // folder ID
path: string; // folder path
}) => Promise<{ success: boolean }>
onMoveAsset: (params: {
assetId: string; // asset ID to move
fromFolder: string; // source folder path
toFolder: string; // destination folder path
}) => Promise<{ success: boolean }>
External Storage Flow
When a user uploads an image in the editor with external storage configured:
- User selects file — The editor captures the file from the upload dialog or drag-and-drop.
getPresignUrlcalled — Editor calls your callback withfileName,fileType,fileSize, and optionalfolder. Your backend generates a presigned S3 PUT URL (or equivalent) and returnsuploadUrl+fileUrl.- Editor uploads to presigned URL — The editor PUTs the file directly to the
uploadUrlreturned in step 2. The upload goes browser-to-storage with no Dragble server in the middle. onUploadcalled — After the upload succeeds, the editor callsonUploadso your backend can record the asset in its database and confirm the final URL.- Image inserted — The confirmed
urlfromonUploadis inserted into the design. - Asset manager browse — When the user opens the asset manager,
onLoadAssetsandonLoadFoldersare called to populate the UI. - Asset operations — Delete, move, and folder management trigger the corresponding optional callbacks.
Complete External Storage Example
dragble.init({
containerId: 'editor-container',
projectId: 'proj_xxx',
assetStorage: {
mode: 'external',
getPresignUrl: async ({ fileName, fileType, fileSize, folder }) => {
const res = await fetch('https://your-api.com/storage/presign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ fileName, fileType, fileSize, folder })
});
return res.json(); // { uploadUrl, fileUrl }
},
onUpload: async ({ fileUrl, fileName, fileType, fileSize, folder }) => {
const res = await fetch('https://your-api.com/storage/confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ fileUrl, fileName, fileType, fileSize, folder })
});
return res.json(); // { url, id }
},
onLoadAssets: async ({ folder, page, perPage, search }) => {
const params = new URLSearchParams({
...(folder && { folder }),
...(page && { page: String(page) }),
...(perPage && { perPage: String(perPage) }),
...(search && { search })
});
const res = await fetch(`https://your-api.com/storage/assets?${params}`, {
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});
return res.json(); // { assets: [...], total }
},
onDeleteAsset: async ({ id, url }) => {
const res = await fetch(`https://your-api.com/storage/assets/${id}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});
return res.json(); // { success: true }
},
onLoadFolders: async ({ parentFolder }) => {
const params = parentFolder ? `?parent=${encodeURIComponent(parentFolder)}` : '';
const res = await fetch(`https://your-api.com/storage/folders${params}`, {
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});
return res.json(); // { folders: [...] }
},
onCreateFolder: async ({ name, parentFolder }) => {
const res = await fetch('https://your-api.com/storage/folders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ name, parentFolder })
});
return res.json(); // { id, name, path }
},
onDeleteFolder: async ({ id, path }) => {
const res = await fetch(`https://your-api.com/storage/folders/${id}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});
return res.json(); // { success: true }
},
onMoveAsset: async ({ assetId, fromFolder, toFolder }) => {
const res = await fetch(`https://your-api.com/storage/assets/${assetId}/move`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ fromFolder, toFolder })
});
return res.json(); // { success: true }
}
}
});
Storage Utility Methods
After initialization, you can query and interact with storage:
// Check if external storage is configured
const isExternal = dragble.isExternalStorage();
// Returns: true if assetStorage.mode === 'external'
// Get the current storage mode
const mode = dragble.getAssetStorageMode();
// Returns: 'dragble' | 'external'
// Programmatically upload an image (works with both modes)
const result = await dragble.uploadImage(file);
// file: File object
// Returns: { url: string } — the public URL of the uploaded image
Common Mistakes & Troubleshooting
NEVER import from @dragble/editor-sdk
The SDK is loaded via CDN script tag. There is no npm package to import.
// WRONG - will not work
import { DragbleSDK } from '@dragble/editor-sdk';
// CORRECT - use the global
dragble.init({ ... });
AI callbacks must return the exact shape
If your callback returns a different structure, the editor will silently fail or show empty results. Match the return types exactly as documented above.
// WRONG - missing wrapper object
generate: async (params) => {
const headings = await fetchHeadings(params);
return headings; // string[] directly
}
// CORRECT - wrapped in expected shape
generate: async (params) => {
const headings = await fetchHeadings(params);
return { headings }; // { headings: string[] }
}
External AI mode with no callbacks shows nothing
If ai.mode is 'external' but you don't provide any feature callbacks, all AI features are silently disabled. The editor does not fall back to Dragble AI in external mode.
Presigned URL must allow PUT from browser
Your S3 presigned URL must have proper CORS headers allowing PUT from the editor's origin. Common issues:
- Missing
Access-Control-Allow-Originheader on the S3 bucket CORS config - Presigned URL expired before the upload completed (use at least 5 minutes expiry)
Content-Typemismatch between the presigned URL and the actual upload
onUpload must return the final public URL
The URL returned by onUpload is what gets embedded in the design HTML. If it returns a temporary or internal URL, images will break when the email is sent.
AI and storage are independent
Setting ai.mode: 'disabled' does not affect asset storage. Setting assetStorage.mode: 'external' does not affect AI. Configure each separately.
Dragble AI requires an active subscription
If you set ai.mode: 'dragble' but the project's subscription has no AI credits remaining, AI requests will fail with a credits-exhausted error. The editor will show an appropriate message to the user.
Image generation URLs must be publicly accessible
URLs returned by imageGeneration.generate must be publicly accessible (no auth required). The editor inserts these URLs directly into the design. If the URLs require authentication or expire quickly, images will break in the final email.
Do not mix getPresignUrl fields with PUT uploads
If your presigned URL is for a PUT upload, do not return fields in the getPresignUrl response. The fields property is only for POST-based multipart form uploads (e.g., S3 POST policy). Mixing them will cause upload failures.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.