Docs

React UI

Attachments end to end

Add file picker, dropzone, previews, controlled attachment state, and server-side handling.

Attachments start in the composer, become UIAttachment values, and are converted by @anvia/react into core messages before the request reaches your server.

Composer with picker and dropzone

import { Attachment, Composer } from "@anvia/react-ui";

export function AttachmentComposer() {
  return (
    <Composer.Root className="composer">
      <Composer.AttachmentDropzone className="dropzone">
        <Composer.Attachments className="attachment-list">
          {(attachment) => (
            <Attachment.Root className="attachment-row">
              <Attachment.Preview />
              <Attachment.Name />
              <Attachment.Remove>Remove</Attachment.Remove>
            </Attachment.Root>
          )}
        </Composer.Attachments>
        <Composer.AddAttachment accept="image/*,.pdf" multiple>
          Attach image or PDF
        </Composer.AddAttachment>
        <Composer.Input placeholder="Ask about the attached file..." />
        <Composer.Submit>Send</Composer.Submit>
      </Composer.AttachmentDropzone>
    </Composer.Root>
  );
}

Controlled attachments

Use controlled state when the app needs validation, upload progress, or a separate form preview.

import type { UIAttachment } from "@anvia/react";
import { useState } from "react";

const [attachments, setAttachments] = useState<UIAttachment[]>([]);

return (
  <Composer.Root
    attachments={attachments}
    onAttachmentsChange={(next) => {
      setAttachments(next.filter((attachment) => attachment.mediaType !== "application/zip"));
    }}
  >
    <Composer.Attachments keepMounted />
    <Composer.AttachmentInput accept="image/*,.pdf" multiple />
    <Composer.Input />
    <Composer.Submit />
  </Composer.Root>
);

Sending attachments manually

When a design system owns the uploader, pass attachments to sendMessage(...).

await chat.sendMessage({
  text: "Summarize these files.",
  attachments,
  metadata: {
    source: "custom-uploader",
  },
});

Server handling

The route still receives core messages:

import type { UIStreamRequest } from "@anvia/core";

export async function POST(request: Request) {
  const body = (await request.json()) as UIStreamRequest;

  return createEventStream(agent.prompt(body.messages).stream(), {
    format: "jsonl",
  });
}

The browser should not receive provider credentials or direct upload authority. If files must be stored before model execution, upload them to your application first and pass safe attachment metadata or URLs through the message.

Attachment UI states

.dropzone {
  border: 1px dashed #52525b;
  border-radius: 14px;
  padding: 12px;
}

.dropzone[data-dragging] {
  border-color: #2bf563;
  background: rgba(43, 245, 99, 0.08);
}

.attachment-row {
  display: inline-flex;
  max-width: 240px;
  align-items: center;
  gap: 8px;
}

.attachment-row [data-anvia-attachment-name] {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

Checklist

  • Limit accepted file types with accept.
  • Validate file count and size in application code.
  • Keep provider keys on the server.
  • Prefer product-owned upload/storage for large files.
  • Render filenames with truncation so long names do not break the composer.