Skip to content

feat(attachments): image and PDF attachments for widget and worker - #150

Merged
PAMulligan merged 3 commits into
mainfrom
53-file-and-image-attachments
Sep 1, 2026
Merged

feat(attachments): image and PDF attachments for widget and worker#150
PAMulligan merged 3 commits into
mainfrom
53-file-and-image-attachments

Conversation

@PAMulligan

Copy link
Copy Markdown
Contributor

Description

Lets visitors attach images (JPEG/PNG/GIF/WebP) and PDFs to a message and have Claude analyze them via native image / document content blocks.

Widget

  • New attachments option (React prop, window.ClaudiusConfig key, and <claudius-chat attachments> attribute). Off by default; true enables the defaults (5 MB per file, 5 files per message), or pass { maxSizeBytes, maxCount, allowedTypes }.
  • Click-to-upload (paperclip), drag-and-drop onto the composer, and paste from the clipboard, with client-side validation and localized error messages in en/es/fr/de.
  • Image thumbnails and PDF filename chips for pending files and inside message bubbles.
  • ChatApiClient switches to multipart/form-data whenever a message carries inline bytes. Persisted history never contains bytes. When the worker stores an upload (R2 mode) the widget keeps the storage key + signed URL instead of the data. A worker-rejected upload rolls the message back and shows a translated error.

Worker

  • attachments.ts: JSON and multipart request parsing, MIME allowlist plus magic-byte verification, per-file / per-message / per-request caps, and Anthropic content-block construction (files first, text last).
  • attachment-storage.ts: passthrough (default, nothing stored) or r2 (stored under att/<tenant>/<uuid> for ATTACHMENT_RETENTION_HOURS, with HMAC-signed download URLs served by GET /api/attachments/*, lazy deletion after expiry).
  • attachment-quota.ts: per-IP and per-tenant daily upload-byte quotas in the existing RATE_LIMIT KV.
  • New error codes: ATTACHMENTS_DISABLED (400), ATTACHMENT_INVALID (400), ATTACHMENT_TOO_LARGE (413), ATTACHMENT_QUOTA_EXCEEDED (413 + Retry-After).

Config & docs

  • clients/_schema.json, CLI validation, and snippet generation support widget.attachments and worker.attachments.
  • New docs page configuration/attachments.md covering limits, storage backends, and the privacy posture (where files live, how long, who can read them), plus updates to the REST reference, widget/worker/clients config pages, FAQ, wrangler.toml, and .dev.vars.example.

Design notes for reviewers:

  • The wire format stays backward compatible: plain conversations still post JSON with string content.
  • Quotas count only the newest message's inline uploads so passthrough re-sends of history don't burn the budget; the per-request byte budget (ATTACHMENT_MAX_REQUEST_BYTES) drops history bytes oldest-first.
  • Cloudflare R2 has no native presigned URLs without S3 keys, so signed links are worker-verified HMACs over key:exp.

Related Issue

Closes #53

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Chore (dependency updates, CI changes, refactoring)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • Tests pass (pnpm test in both widget/ and worker/) — widget 323, worker 97, scripts 56
  • Linting passes (pnpm lint in widget/)
  • Formatting passes (pnpm format:check in widget/)
  • Types check (pnpm typecheck in widget/)
  • Widget builds successfully (cd widget && pnpm build)
  • Documentation updated (if applicable)
  • Branch name follows convention (feat/, fix/, docs/, chore/) — uses the repo's existing <issue>-<slug> pattern

Screenshots / Notes

  • Not verified end to end against a live Anthropic key or a real R2 bucket; the worker paths are covered by route-level tests with mocked SDK/KV/R2. The docs site was not built locally (no docs/node_modules).
  • Storybook: ChatInput › WithAttachments / WithPendingAttachments and ChatMessage › WithAttachments.

🤖 Generated with Claude Code

Closes #53.

Widget
- `attachments` prop / ClaudiusConfig key / `<claudius-chat attachments>`
  attribute (off by default). Click-to-upload, drag-and-drop, and paste in
  the composer with client-side type/size/count validation (5 MB, 5 files,
  JPEG/PNG/GIF/WebP/PDF by default) and localized errors (en/es/fr/de).
- Image previews and PDF filename chips in pending state and message bubbles.
- ChatApiClient switches to multipart/form-data when a message carries inline
  bytes; persisted history never stores bytes; stored uploads are swapped for
  the worker's key + signed URL; rejected uploads roll the message back.

Worker
- `attachments.ts`: JSON + multipart parsing, allowlist + magic-byte checks,
  size/count caps, per-request byte budget, image/document content blocks.
- `attachment-storage.ts`: passthrough (default) or R2 with retention,
  HMAC-signed download URLs served by GET /api/attachments/*.
- `attachment-quota.ts`: per-IP and per-tenant daily upload quotas in KV.
- New error codes: ATTACHMENTS_DISABLED, ATTACHMENT_INVALID,
  ATTACHMENT_TOO_LARGE, ATTACHMENT_QUOTA_EXCEEDED.

Config and docs
- Client config schema + CLI validation/snippets for widget.attachments and
  worker.attachments.
- New docs page configuration/attachments.md (limits, storage backends,
  privacy posture) plus REST, widget, worker, clients, and FAQ updates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploying chat-widget with  Cloudflare Pages  Cloudflare Pages

Latest commit: fa6abc4
Status: ✅  Deploy successful!
Preview URL: https://d60291fc.chat-widget-ejc.pages.dev
Branch Preview URL: https://53-file-and-image-attachment.chat-widget-ejc.pages.dev

View logs

Resolve conflicts with the streaming, tool-use, and RAG work that landed on
main since the branch was cut:

- worker: attachments now flow through a shared prepareAttachments() step
  used by both POST /api/chat and POST /api/chat/stream; the stream route
  accepts multipart bodies, reports stored uploads on the done event, and
  attachment/storage errors are folded into classifyChatError.
- worker: validateMessages keeps attachments; both handleChat and streamChat
  convert them to image/document content blocks via toConversation().
- widget: the client sends the same JSON-or-multipart body to the stream
  endpoint and surfaces done-event attachments; useChat applies stored
  attachment metadata after either path and keeps the attachment rollback
  ahead of the partial-stream error handling.
- widget: ChatInput/ChatMessage/ChatWindow/ChatWidget carry both the
  streaming (stop button, tool chips) and attachment props.
- tests: stream-route and stream-client attachment coverage; make the
  signed-URL tampering assertion deterministic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
claudius.iife.js (raw) 193.66 KB (+6.14% 🔺)
claudius.iife.js (gzip) 61.97 KB (+6.16% 🔺)
claudius.iife.js (brotli) 54.28 KB (+5.94% 🔺)
claudius.js (raw) 80.01 KB (+24.94% 🔺)
claudius.js (gzip) 21.57 KB (+23.5% 🔺)
claudius.js (brotli) 18.81 KB (+23.71% 🔺)
claudius.css (raw) 21.59 KB (+7.02% 🔺)
claudius.css (gzip) 4.68 KB (+4.77% 🔺)
claudius.css (brotli) 4.1 KB (+4.93% 🔺)

The attachment composer (file picker, drag-and-drop, paste, previews),
multipart client path, and message-bubble previews add ~3.6 KB gzip to
the minified embed bundle, ~4.2 KB gzip to the unminified ESM library,
and ~220 B gzip of CSS. New limits are the measured sizes plus the usual
5% headroom, matching the tool-use budget bump.

Refs #53

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@PAMulligan
PAMulligan merged commit 95a3c7a into main Sep 1, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File and image attachments (PDF/image upload, multimodal messages)

1 participant