agentsclimarketplace

Dragble ai

Skill Dragble/dragble-skills/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

Install
npx -y skills add Dragble/dragble-skills --skill dragble-ai

Assembled 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:

ModeDescriptionRequires
dragbleBuilt-in AI powered by Dragble servers (paid plans)Active subscription with AI credits
externalBring Your Own Key / backend — works on any planYou provide callback functions that call your own API
disabledAll AI features hidden from the editor UINothing

Asset Storage — Upload, browse, and manage images used in designs. Two modes:

ModeDescriptionRequires
dragbleManaged cloud storage (Cloudflare R2) — zero configActive subscription (default)
externalYour own S3-compatible bucket or custom backendYou 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'
  }
});
ModeUI VisibleCredits UsedCallbacks Required
dragbleYesYes (Dragble credits)No
externalYesNoYes — you provide one or more feature callbacks
disabledNo — all AI buttons/menus hiddenNoNo

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:

  1. disabled — If ai.mode is 'disabled', all AI features are hidden. Stop.
  2. feature.enabled: false — If a specific feature has enabled: false, that feature is hidden. Skip it.
  3. Has callback — If the feature has a generate callback, use external mode for that feature regardless of global mode.
  4. Global mode — Fall back to the global ai.mode setting ('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'
  }
});
ModeUpload TargetAsset ManagementConfig Required
dragbleDragble managed cloud (Cloudflare R2)Built-in asset managerNone (default)
externalYour S3-compatible bucket or custom backendYou provide callbacksYes — 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:

CallbackRequiredPurpose
getPresignUrlYesGenerate a presigned upload URL for the file
onUploadYesConfirm upload completion, return the final public URL
onLoadAssetsOptionalList/paginate assets in the asset manager
onDeleteAssetOptionalDelete an asset from your storage
onLoadFoldersOptionalList folders in the asset manager
onCreateFolderOptionalCreate a new folder
onDeleteFolderOptionalDelete a folder
onMoveAssetOptionalMove 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:

  1. User selects file — The editor captures the file from the upload dialog or drag-and-drop.
  2. getPresignUrl called — Editor calls your callback with fileName, fileType, fileSize, and optional folder. Your backend generates a presigned S3 PUT URL (or equivalent) and returns uploadUrl + fileUrl.
  3. Editor uploads to presigned URL — The editor PUTs the file directly to the uploadUrl returned in step 2. The upload goes browser-to-storage with no Dragble server in the middle.
  4. onUpload called — After the upload succeeds, the editor calls onUpload so your backend can record the asset in its database and confirm the final URL.
  5. Image inserted — The confirmed url from onUpload is inserted into the design.
  6. Asset manager browse — When the user opens the asset manager, onLoadAssets and onLoadFolders are called to populate the UI.
  7. 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-Origin header on the S3 bucket CORS config
  • Presigned URL expired before the upload completed (use at least 5 minutes expiry)
  • Content-Type mismatch 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.

Keep looking

Skills are one crate of 326,629. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.