OSAGI

Attachments Tray

AI chats

About

Attachments tray for AI chat: drop files or pick them, see each upload's progress, ready or a plain error, remove before sending. Rows slide in on a spring, a live progress bar per file. From the AI Assistant Kit → ai.osagi.finance/kits/ai-assistant

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Attachments Tray to your project and fits it to your theme.

Code

How Attachments Tray is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import {
  AttachmentsTray,
  type TrayFile,
} from "@/components/ui/attachments-tray";

const settings = {
  failOverMb: 25,
  showSamples: true,
  uploadMs: 1800,
};

const SAMPLES: TrayFile[] = [
  {
    id: "sample-1",
    name: "quote-north-office.pdf",
    progress: 1,
    size: 482_000,
    status: "ready",
  },
  {
    id: "sample-2",
    name: "travel-budget.xlsx",
    progress: 0.35,
    size: 1_900_000,
    status: "uploading",
  },
];
const NONE: TrayFile[] = [];
const STEP_MS = 100;
const MB = 1024 * 1024;

/** One tick of a simulated upload: `step` of the way, "ready" at the end. */
function advance(file: TrayFile, step: number): TrayFile {
  if (file.status !== "uploading") {
    return file;
  }
  const progress = Math.min(1, (file.progress ?? 0) + step);
  return progress < 1
    ? { ...file, progress }
    : { ...file, progress: 1, status: "ready" };
}

type Settings = typeof settings;

/**
 * The tray with simulated uploads: one timer moves every upload on while any is running, at the
 * speed `uploadMs` sets now (a change applies to the running ones too); it stops on unmount.
 */
function SimulatedTray({ failOverMb, showSamples, uploadMs }: Settings) {
  const [files, setFiles] = useState(showSamples ? SAMPLES : NONE);
  const speed = useRef(uploadMs);
  useEffect(() => {
    speed.current = uploadMs;
  }, [uploadMs]);
  const isUploading = files.some((file) => file.status === "uploading");
  useEffect(() => {
    if (!isUploading) {
      return;
    }
    const timer = setInterval(() => {
      const step = STEP_MS / Math.max(STEP_MS, speed.current);
      setFiles((current) => current.map((file) => advance(file, step)));
    }, STEP_MS);
    return () => clearInterval(timer);
  }, [isUploading]);

  const onAdd = useCallback(
    (added: File[]) => {
      const stamp = Date.now();
      const rows = added.map((file, index): TrayFile => {
        const id = `${stamp}-${index}-${file.name}`;
        return file.size > failOverMb * MB
          ? {
              error: `Larger than ${failOverMb} MB`,
              id,
              name: file.name,
              size: file.size,
              status: "error",
            }
          : {
              id,
              name: file.name,
              progress: 0,
              size: file.size,
              status: "uploading",
            };
      });
      setFiles((current) => [...current, ...rows]);
    },
    [failOverMb]
  );
  const onRemove = useCallback(
    (id: string) =>
      setFiles((current) => current.filter((file) => file.id !== id)),
    []
  );

  return <AttachmentsTray files={files} onAdd={onAdd} onRemove={onRemove} />;
}

/** Uploads are simulated: progress climbs over `uploadMs`; files over `failOverMb` fail. */
export default function Demo(props: Partial<Settings>) {
  const s = { ...settings, ...props };
  return (
    <div className="flex min-h-[520px] w-full items-start justify-center bg-background p-6">
      {/* Switching the samples starts the demo over. */}
      <SimulatedTray key={String(s.showSamples)} {...s} />
    </div>
  );
}

Dependencies

More by OSAGI

View all
  • AI Orb

    AI Orb

    83
  • KPI Tile

    KPI Tile

    28
  • OTP Input

    OTP Input

    12
  • Reasoning Thread

    Reasoning Thread

    8
  • Assistant Composer

    Assistant Composer

    8
  • Release Notes

    Release Notes

  • Docs TOC

    Docs TOC

  • Docs Search

    Docs Search

More in AI chats

View all
  • AI Actions

    AI Actions

    319
  • Color Picker

    Color Picker

    82
  • Agent Chat

    Agent Chat

    17
  • Prompt with Model Picker

    Prompt with Model Picker

    2
  • System Message

    System Message

    54
  • Message Thread

    Message Thread

    36
  • Suggestions

    Suggestions

    28
  • Message With Header And Delivery Status

    Message With Header And Delivery Status

    38