"use client"; import * as React from "react"; import { Check, Library, Search } from "lucide-react"; import { ResponsiveDialog } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ChipRow } from "@/components/ui/segmented"; import { EmptyState, Skeleton } from "@/components/ui/misc"; import { toast } from "@/components/ui/toast"; import { useApi, api } from "@/lib/client/api"; import { useApp } from "@/components/app/store"; import { errorMessage } from "@/lib/client/humanize"; import type { PublicProjectFile, PublicProject } from "@/lib/client/types"; import { formatTokens, formatRelative, cn } from "@/lib/utils"; import { FileThumb } from "./file-list"; import { formatBytes, kindLabel } from "./format"; import { UploadButton } from "./upload-button"; export interface PickedAttachment { id: string; kind: string; name: string; mimeType: string; sizeBytes: number; width?: number | null; height?: number | null; } type Scope = "project" | "global" | "all"; const MAX_PICK = 10; /** * Pick stored files (project / global library) to attach to a new message. * Multi-select (max 10), search, scope chips (this project / global / all), inline upload. * Calls POST /api/library/files/attach which copies the files into `message_attachments` and returns * pending attachment descriptors compatible with the composer (same shape as POST /api/attachments). */ export function FileLibraryPicker({ open, onOpenChange, onPick, projectId }: { open: boolean; onOpenChange: (o: boolean) => void; onPick: (files: PickedAttachment[]) => void; projectId?: string | null }) { const { activeProjectId } = useApp(); const pid = projectId ?? activeProjectId; const { data, isLoading, mutate } = useApi<{ files: PublicProjectFile[] }>(open ? "/api/library/files" : null); const { data: projData } = useApi<{ projects: PublicProject[] }>(open ? "/api/projects" : null); const [selected, setSelected] = React.useState>(new Set()); const [q, setQ] = React.useState(""); const [scope, setScope] = React.useState(pid ? "project" : "all"); const [busy, setBusy] = React.useState(false); React.useEffect(() => { if (!open) return; // eslint-disable-next-line react-hooks/set-state-in-effect setSelected(new Set()); setQ(""); setScope(pid ? "project" : "all"); }, [open, pid]); const project = React.useMemo(() => (pid ? projData?.projects.find((p) => p.id === pid) : undefined), [pid, projData]); const all = React.useMemo(() => data?.files ?? [], [data]); const files = React.useMemo(() => { const s = q.trim().toLowerCase(); return all .filter((f) => (scope === "project" ? f.projectId === pid : scope === "global" ? !f.projectId : true)) .filter((f) => (s ? f.name.toLowerCase().includes(s) || (f.description ?? "").toLowerCase().includes(s) || kindLabel(f.kind).toLowerCase().includes(s) : true)); }, [all, scope, pid, q]); const counts = React.useMemo(() => ({ project: all.filter((f) => f.projectId === pid).length, global: all.filter((f) => !f.projectId).length, all: all.length }), [all, pid]); const toggle = (id: string) => setSelected((s) => { const n = new Set(s); if (n.has(id)) n.delete(id); else if (n.size < MAX_PICK) n.add(id); else toast.warning(`You can attach up to ${MAX_PICK} files at once`); return n; }); const selectedTokens = React.useMemo(() => all.filter((f) => selected.has(f.id)).reduce((n, f) => n + (f.estimatedTokens ?? 0), 0), [all, selected]); const attach = async () => { setBusy(true); try { const res = await api<{ attachments: PickedAttachment[] }>("/api/library/files/attach", { method: "POST", json: { fileIds: [...selected] } }); onPick(res.attachments); onOpenChange(false); } catch (e) { toast.error("Could not attach files", errorMessage(e)); } finally { setBusy(false); } }; return (
setQ(e.target.value)} placeholder="Search files…" className="h-10 pl-8 sm:h-9" aria-label="Search files" />
{pid ? : null} } footer={
{selected.size ? `${selected.size} selected${selectedTokens ? ` · ~${formatTokens(selectedTokens)} tokens` : ""}` : "Nothing selected"}
mutate()}> Upload
} >
{isLoading && !data ? (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) : files.length === 0 ? ( } title={all.length ? "No matching files" : "No stored files yet"} description={all.length ? "Try another search or scope." : "Upload PDFs, images or documents once to a project and reuse them in any chat."} className="my-4 border-0 py-8" /> ) : (
    {files.map((f) => { const on = selected.has(f.id); return (
  • ); })}
)}
); }