SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.8 KB · 87 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Upload } from "lucide-react";4import { Button, type ButtonProps } from "@/components/ui/button";5import { toast } from "@/components/ui/toast";6import { errorMessage } from "@/lib/client/humanize";7import { ClientApiError } from "@/lib/client/api";8import { LIBRARY_ACCEPT } from "./format";9import type { PublicProjectFile } from "@/lib/client/types";1011export const LIBRARY_MAX_BYTES = 10 * 1024 * 1024;1213/** Uploads files one by one to the library (multipart, same limits as chat attachments). Returns saved files + errors. */14export async function uploadLibraryFiles(files: File[], projectId?: string | null, onProgress?: (done: number, total: number) => void): Promise<{ saved: PublicProjectFile[]; errors: { name: string; message: string }[] }> {15  const saved: PublicProjectFile[] = [];16  const errors: { name: string; message: string }[] = [];17  let done = 0;18  for (const f of files) {19    if (f.size > LIBRARY_MAX_BYTES) {20      errors.push({ name: f.name, message: "Larger than 10 MB" });21    } else {22      const form = new FormData();23      form.append("file", f);24      if (projectId) form.append("projectId", projectId);25      try {26        const res = await fetch("/api/library/files", { method: "POST", body: form, credentials: "same-origin" });27        if (!res.ok) {28          const body = (await res.json().catch(() => ({}))) as { error?: { code?: string; message?: string } };29          throw new ClientApiError(res.status, body.error?.code ?? "HTTP_ERROR", body.error?.message ?? `Upload failed (${res.status})`);30        }31        const body = (await res.json()) as { file: PublicProjectFile };32        saved.push(body.file);33      } catch (e) {34        errors.push({ name: f.name, message: errorMessage(e) });35      }36    }37    done += 1;38    onProgress?.(done, files.length);39  }40  return { saved, errors };41}4243/** Summarises an upload batch in toasts. */44export function reportUpload(result: { saved: PublicProjectFile[]; errors: { name: string; message: string }[] }) {45  if (result.saved.length) toast.success(result.saved.length === 1 ? `Added “${result.saved[0].name}”` : `Added ${result.saved.length} files to the library`);46  for (const e of result.errors.slice(0, 3)) toast.error(`Could not upload ${e.name}`, e.message);47  if (result.errors.length > 3) toast.error(`${result.errors.length - 3} more files failed`);48}4950export function UploadButton({ projectId, onUploaded, children, size = "md", variant = "primary", className, ...rest }: { projectId?: string | null; onUploaded?: (files: PublicProjectFile[]) => unknown } & Omit<ButtonProps, "onClick">) {51  const inputRef = React.useRef<HTMLInputElement>(null);52  const [busy, setBusy] = React.useState<{ done: number; total: number } | null>(null);5354  const onFiles = async (list: FileList | null) => {55    const files = list ? Array.from(list) : [];56    if (!files.length) return;57    setBusy({ done: 0, total: files.length });58    try {59      const result = await uploadLibraryFiles(files, projectId, (done, total) => setBusy({ done, total }));60      reportUpload(result);61      if (result.saved.length) await onUploaded?.(result.saved);62    } finally {63      setBusy(null);64      if (inputRef.current) inputRef.current.value = "";65    }66  };6768  return (69    <>70      <input ref={inputRef} type="file" multiple accept={LIBRARY_ACCEPT} className="sr-only" tabIndex={-1} aria-hidden onChange={(e) => void onFiles(e.target.files)} />71      <Button size={size} variant={variant} className={className} loading={Boolean(busy)} onClick={() => inputRef.current?.click()} {...rest}>72        {busy ? (73          <span>74            {busy.done}/{busy.total}75          </span>76        ) : (77          children ?? (78            <>79              <Upload /> Upload80            </>81          )82        )}83      </Button>84    </>85  );86}87