Skip to content
kitn AI/UI

Attachments flow

<kai-prompt-input> ships with an attach button. Click the paperclip, pick a file, send — kai-submit delivers { value, attachments } together, and the user’s <kai-message> renders the files inline. You own the upload logic; AI/UI handles the staging and display.

The paperclip, the removable chips, and the staged-file state are all built in. You don’t wire an upload component for the common case — you just read kai-submit.

1 — Read the payload from kai-submit

The composer stages files internally as the user picks them. On submit, kai-submit carries the staged list alongside the text — always an array, even when empty:

import '@kitn.ai/ui/elements';
const prompt = document.getElementById('prompt');
prompt.addEventListener('kai-submit', async (e) => {
const { value, attachments } = e.detail;
// attachments: AttachmentData[] — always an array, never undefined
const aId = crypto.randomUUID();
chat.messages = [
...chat.messages,
{
id: crypto.randomUUID(),
role: 'user',
parts: [
// one `file` part per attachment → shown inline on the message
...attachments.map((a) => ({ type: 'file', attachment: a })),
...(value ? [{ type: 'text', text: value }] : []),
],
},
{ id: aId, role: 'assistant', parts: [] },
];
chat.loading = true;
// Forward value + attachments to your model API, then stream the reply.
let answer = '';
for await (const token of streamFromModel({ value, attachments })) {
answer += token;
chat.messages = chat.messages.map((m) =>
m.id === aId ? { ...m, parts: [{ type: 'text', text: answer }] } : m,
);
}
chat.loading = false;
});

<kai-chat> carries the same composer and fires the same kai-submit — the paperclip works there with zero extra setup too.

2 — Render attachments on the user message

Add a { type: 'file', attachment } part per file to the message’s parts array. <kai-chat> and <kai-message> render consecutive file parts inline beneath the message text — the same display the <kai-attachments> element uses — with no extra wiring:

// The message shape — file parts are optional; omit them for text-only turns.
const userMsg = {
id: crypto.randomUUID(),
role: 'user',
parts: [
{ type: 'file', attachment: { id: 'a1', type: 'file', filename: 'design-spec.pdf', mediaType: 'application/pdf' } },
{ type: 'file', attachment: { id: 'a2', type: 'file', filename: 'screenshot.png', mediaType: 'image/png' } },
{ type: 'text', text: value },
],
};

AttachmentData shape:

FieldRequiredNotes
idYesStable identifier — used for removals via the chip’s remove button
typeYes'file' or 'source-document'
filenameNoShown as the chip label
mediaTypeNoMIME type — drives the icon (image / video / audio / document)
urlNoObject URL or CDN URL — enables image previews in the hover card
titleNoDisplay name for source-document attachments

Assign prompt.attachments in JavaScript after mount to seed the composer before the user types — for files already linked to this conversation, say. The element manages its own staged list from there: the user can add more with the paperclip, remove chips, and kai-submit always delivers the current state:

prompt.attachments = [
{ id: 'ctx-1', type: 'file', filename: 'brief.pdf', mediaType: 'application/pdf' },
];

When you want a large drop target for a whole page or panel — not just the composer’s paperclip — add a standalone <kai-file-upload> and feed its files into the composer. The round-trip from kai-submit onward is identical; you’re only changing how files get staged.

<kai-file-upload> fires kai-files-added when the user picks or drops files. Convert each File to an AttachmentData and append it to prompt.attachments:

const upload = document.getElementById('upload');
const prompt = document.getElementById('prompt');
upload.addEventListener('kai-files-added', (e) => {
const current = prompt.attachments ?? [];
const added = e.detail.files.map((f) => ({
id: crypto.randomUUID(),
type: 'file',
filename: f.name,
mediaType: f.type || undefined,
// create an object URL if you want an image preview in the hover card
url: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined,
}));
prompt.attachments = [...current, ...added];
});

The drop zone and the paperclip stack: files from either path land in the same staged list and ride along on the next kai-submit.