# 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.

- Skill: `dragble/dragble-ai` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dragble/dragble-ai`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dragble/dragble-ai/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: Dragble (https://skillmd.com/u/dragble)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dragble/dragble-ai

---


# 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()`:

```js
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)

```js
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

```js
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:

```js
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:**

```ts
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:**

```js
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:**

```ts
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:**

```js
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:**

```ts
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:**

```js
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:**

```ts
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:**

```js
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:**

```ts
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:**

```js
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:**

```ts
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:**

```js
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:

```js
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:

```js
// 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()`:

```js
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:

```js
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:**

```ts
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

```js
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:

```js
// 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.

```js
// 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.

```js
// 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.

