Attachments

Send images, PDFs, and files alongside chat messages

Send images, PDFs, and files alongside chat messages for AI analysis.


Overview

User: [Attaches screenshot.png]
User: "What's wrong with this error?"
AI: "I can see a TypeError in your console. The issue is..."

Enable Attachments

<CopilotChat
  attachmentsEnabled={true}
/>

Send Message with Attachment

const { sendMessage } = useCopilot();

// Send with image
await sendMessage("Analyze this chart", [
  {
    type: 'image',
    data: base64ImageData,
    mimeType: 'image/png',
    filename: 'chart.png',
  },
]);

// Send with file
await sendMessage("Summarize this document", [
  {
    type: 'file',
    data: base64PdfData,
    mimeType: 'application/pdf',
    filename: 'report.pdf',
  },
]);

Attachment Types

interface MessageAttachment {
  type: 'image' | 'file' | 'audio' | 'video';
  data: string;        // Base64 encoded or URL
  mimeType: string;    // MIME type
  filename?: string;   // Display name
  url?: string;        // If stored in cloud
}

Supported Types

TypeMIME TypesNotes
imageimage/png, image/jpeg, image/gif, image/webpAI can analyze visually
fileapplication/pdf, text/plain, text/csvAI extracts text content
audioaudio/mp3, audio/wavTranscription (model dependent)
videovideo/mp4Frame extraction (model dependent)

Configuration

<CopilotChat
  attachmentsEnabled={true}
  maxFileSize={10 * 1024 * 1024}  // 10MB
  allowedFileTypes={[
    'image/*',
    'application/pdf',
    '.csv',
    '.txt',
  ]}
/>

Props

PropTypeDefaultDescription
attachmentsEnabledbooleanfalseEnable attachment button
maxFileSizenumber5MBMaximum file size in bytes
allowedFileTypesstring[]AllAllowed MIME types or extensions
uploadstring | object | function-Upload handler (see below)

The upload Prop

One prop, three modes — from simple to full control:

// 1. URL string — server handles the upload
<CopilotChat upload="/api/copilot/upload" />

// 2. Object — URL + custom headers/body
<CopilotChat upload={{
  url: "/api/copilot/upload",
  headers: () => ({ Authorization: `Bearer ${token}` }),
  body: { projectId: "abc" },
}} />

// 3. Function — full custom logic
<CopilotChat upload={async (file) => {
  const url = await myS3Upload(file);
  return { type: 'image', url, mimeType: file.type, filename: file.name };
}} />

Without upload, files are embedded as base64 in the message body (works but heavier payload).


Point upload to your server endpoint. Files are uploaded there, and the returned URL is sent with the message.

<CopilotChat
  attachmentsEnabled={true}
  upload="/api/copilot/upload"
/>

With YourGPT

If your server uses the YourGPT adapter, the upload endpoint uses the same credentials:

server.ts
import { createYourGPT } from '@yourgpt/llm-sdk/yourgpt';

const yourgpt = createYourGPT({ apiKey, widgetUid });

app.post('/api/copilot/upload', async (req, res) => {
  const result = await yourgpt.uploadFile(req.body);
  res.json(result);
});

With Custom Storage

server.ts
app.post('/api/copilot/upload', async (req, res) => {
  const { data, mimeType, filename } = req.body;
  const url = await myStorage.upload(Buffer.from(data, 'base64'), mimeType);
  res.json({ url });
});

With Auth Headers

<CopilotChat
  upload={{
    url: "/api/copilot/upload",
    headers: () => ({ Authorization: `Bearer ${getToken()}` }),
  }}
/>

Custom Upload Function

For complete control — handle the file however you want:

<CopilotChat
  attachmentsEnabled={true}
  upload={async (file: File) => {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    });
    const { url } = await response.json();

    return {
      type: 'image',
      url,
      mimeType: file.type,
      filename: file.name,
    };
  }}
/>

Cloud storage is recommended for production. Base64 embedding works but increases message size significantly.


Programmatic Attachments

From File Input

function FileUploader() {
  const { sendMessage } = useCopilot();

  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    // Convert to base64
    const base64 = await fileToBase64(file);

    await sendMessage("Please analyze this file", [{
      type: file.type.startsWith('image/') ? 'image' : 'file',
      data: base64,
      mimeType: file.type,
      filename: file.name,
    }]);
  };

  return <input type="file" onChange={handleFileSelect} />;
}

// Helper function
function fileToBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = reader.result as string;
      // Remove data URL prefix
      resolve(result.split(',')[1]);
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

From Canvas/Screenshot

async function sendScreenshot() {
  const { sendMessage } = useCopilot();

  // Capture using html2canvas or similar
  const canvas = await html2canvas(document.body);
  const base64 = canvas.toDataURL('image/png').split(',')[1];

  await sendMessage("What do you see on this screen?", [{
    type: 'image',
    data: base64,
    mimeType: 'image/png',
    filename: 'screenshot.png',
  }]);
}

From URL

await sendMessage("Analyze this image", [{
  type: 'image',
  url: 'https://example.com/chart.png',
  mimeType: 'image/png',
}]);

Display Attachments

Attachments appear in message history:

function MessageAttachments({ attachments }) {
  if (!attachments?.length) return null;

  return (
    <div className="flex gap-2 mt-2">
      {attachments.map((attachment, i) => (
        <AttachmentPreview key={i} attachment={attachment} />
      ))}
    </div>
  );
}

function AttachmentPreview({ attachment }) {
  if (attachment.type === 'image') {
    const src = attachment.url || `data:${attachment.mimeType};base64,${attachment.data}`;
    return (
      <img
        src={src}
        alt={attachment.filename}
        className="max-w-xs rounded-lg"
      />
    );
  }

  return (
    <div className="flex items-center gap-2 p-2 bg-muted rounded">
      <FileIcon className="w-4 h-4" />
      <span className="text-sm">{attachment.filename}</span>
    </div>
  );
}

Image Analysis

AI can analyze images when using vision-capable models:

// Works with: GPT-4o, Claude 3, Gemini Pro Vision
await sendMessage("What's in this image?", [{
  type: 'image',
  data: imageBase64,
  mimeType: 'image/png',
}]);

Use Cases

  • Bug reports: Attach screenshots of errors
  • Design feedback: Upload mockups for AI review
  • Data analysis: Send charts for interpretation
  • Document processing: PDF extraction and summarization

Best Practices

1. Compress Images

async function compressImage(file: File): Promise<string> {
  const bitmap = await createImageBitmap(file);
  const canvas = document.createElement('canvas');

  // Resize if too large
  const maxSize = 1920;
  let { width, height } = bitmap;
  if (width > maxSize || height > maxSize) {
    const ratio = Math.min(maxSize / width, maxSize / height);
    width *= ratio;
    height *= ratio;
  }

  canvas.width = width;
  canvas.height = height;

  const ctx = canvas.getContext('2d')!;
  ctx.drawImage(bitmap, 0, 0, width, height);

  return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];
}

2. Validate Before Upload

function validateAttachment(file: File): string | null {
  const maxSize = 10 * 1024 * 1024; // 10MB

  if (file.size > maxSize) {
    return `File too large. Maximum size is ${maxSize / 1024 / 1024}MB`;
  }

  const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf'];
  if (!allowedTypes.includes(file.type)) {
    return `File type not supported. Allowed: ${allowedTypes.join(', ')}`;
  }

  return null; // Valid
}

3. Show Upload Progress

function UploadProgress({ progress }) {
  return (
    <div className="w-full bg-muted rounded-full h-2">
      <div
        className="bg-primary h-2 rounded-full transition-all"
        style={{ width: `${progress}%` }}
      />
    </div>
  );
}

Full Example

function ChatWithAttachments() {
  const { sendMessage } = useCopilot();
  const [uploading, setUploading] = useState(false);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleAttachment = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    // Validate
    const error = validateAttachment(file);
    if (error) {
      toast.error(error);
      return;
    }

    setUploading(true);

    try {
      // Process image
      let data: string;
      if (file.type.startsWith('image/')) {
        data = await compressImage(file);
      } else {
        data = await fileToBase64(file);
      }

      // Send with message
      await sendMessage(`Please analyze this ${file.type.split('/')[0]}`, [{
        type: file.type.startsWith('image/') ? 'image' : 'file',
        data,
        mimeType: file.type,
        filename: file.name,
      }]);
    } catch (err) {
      toast.error('Upload failed');
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input
        ref={fileInputRef}
        type="file"
        className="hidden"
        accept="image/*,application/pdf"
        onChange={handleAttachment}
      />

      <button
        onClick={() => fileInputRef.current?.click()}
        disabled={uploading}
        className="btn-secondary"
      >
        {uploading ? 'Uploading...' : 'Attach File'}
      </button>
    </div>
  );
}

Next Steps

On this page