(null);
const projectById = React.useMemo(() => new Map(projects.map((p) => [p.id, p])), [projects]);
const attachToNewChat = async (f: PublicProjectFile) => {
try {
const { href } = await prepareFilesForNewChat([f.id]);
router.push(contextProjectId ? `${href}&project=${encodeURIComponent(contextProjectId)}` : href);
} catch (e) {
toast.error("Could not attach file", errorMessage(e));
}
};
const download = (f: PublicProjectFile) => {
const a = document.createElement("a");
a.href = `/api/library/files/${f.id}?download=1`;
a.download = f.name;
a.click();
};
const move = async (f: PublicProjectFile, projectId: string | null) => {
try {
await api(`/api/library/files/${f.id}`, { method: "PATCH", json: { projectId } });
await onChanged?.();
toast.success(projectId ? `Moved to ${projectById.get(projectId)?.name ?? "project"}` : "Moved to the global library");
} catch (e) {
toast.error("Could not move file", errorMessage(e));
}
};
const remove = async (f: PublicProjectFile) => {
try {
await api(`/api/library/files/${f.id}`, { method: "DELETE" });
await onChanged?.();
toast.success("File deleted");
} catch (e) {
toast.error("Could not delete file", errorMessage(e));
throw e;
}
};
const itemsFor = (f: PublicProjectFile): (ActionSheetItem | "separator")[] => [
{ key: "use", label: "Use in new chat", icon: , onSelect: () => void attachToNewChat(f) },
{ key: "preview", label: f.kind === "pdf" ? "Open" : "Preview", icon: , onSelect: () => (f.kind === "pdf" ? window.open(`/api/library/files/${f.id}`, "_blank", "noopener") : setPreview(f)) },
{ key: "download", label: "Download", icon: , onSelect: () => download(f) },
"separator",
{ key: "move", label: f.projectId ? "Move to another project…" : "Move to a project…", icon: , onSelect: () => setMoving(f), disabled: projects.length === 0 && !f.projectId },
{ key: "edit", label: "Edit description", icon: , onSelect: () => setEditing(f) },
"separator",
{ key: "delete", label: "Delete", icon: , destructive: true, onSelect: () => setDeleting(f) },
];
if (loading && !files) {
return (
{Array.from({ length: 5 }).map((_, i) => (
))}
);
}
if (!files?.length) return } title={emptyTitle} description={emptyDescription} action={emptyAction} className={className} />;
return (
<>
{files.map((f) => (
(f.kind === "pdf" ? window.open(`/api/library/files/${f.id}`, "_blank", "noopener") : setPreview(f))} />
))}
!o && setPreview(null)} onUse={attachToNewChat} onDownload={download} />
!o && setEditing(null)}
title="Description"
description="Shown in the library and used as context when the file is injected into a prompt."
defaultValue={editing?.description ?? ""}
multiline
maxLength={500}
placeholder="Q3 pricing sheet, includes EU margins…"
onSubmit={async (v) => {
if (!editing) return;
await api(`/api/library/files/${editing.id}`, { method: "PATCH", json: { description: v } });
await onChanged?.();
}}
/>
!o && setMoving(null)} title="Move file" description={moving?.name} size="sm">
moving && (setMoving(null), void move(moving, null))}>
Global library
{!moving?.projectId ? current : null}
{projects
.filter((p) => !p.archived)
.map((p) => (
moving && (setMoving(null), void move(moving, p.id))}>
{p.name}
{moving?.projectId === p.id ? current : null}
))}
!o && setDeleting(null)} destructive title={`Delete “${deleting?.name ?? ""}”?`} description="Messages that already used this file keep their own copy." confirmLabel="Delete" onConfirm={() => (deleting ? remove(deleting) : Promise.resolve())} />
>
);
}
function FileRow({ file, project, items, onOpen }: { file: PublicProjectFile; project?: PublicProject; items: (ActionSheetItem | "separator")[]; onOpen: () => void }) {
const isMobile = useIsMobile();
const [menu, setMenu] = React.useState(false);
const press = useLongPress({ onLongPress: () => setMenu(true), disabled: !isMobile });
return (
{file.name}
{project ? (
{project.icon ?? "◆"} {project.name}
) : null}
{kindLabel(file.kind)} · {formatBytes(file.sizeBytes)}
{file.estimatedTokens ? ` · ~${formatTokens(file.estimatedTokens)} tokens` : ""} · {formatRelative(file.createdAt)}
{project ? · {project.name} : null}
{file.description ? {file.description} : null}
);
}
/** Image / text preview in a sheet (PDFs open in a new tab). */
function FilePreviewSheet({ file, onOpenChange, onUse, onDownload }: { file: PublicProjectFile | null; onOpenChange: (o: boolean) => void; onUse: (f: PublicProjectFile) => void; onDownload: (f: PublicProjectFile) => void }) {
const [text, setText] = React.useState(null);
const isText = file && file.kind !== "image" && file.kind !== "pdf";
React.useEffect(() => {
if (!file || !isText) return;
let cancelled = false;
fetch(`/api/library/files/${file.id}`, { credentials: "same-origin" })
.then((r) => r.text())
.then((t) => {
if (!cancelled) setText(t.length > 40_000 ? `${t.slice(0, 40_000)}\n… (${formatBytes(file.sizeBytes)} total)` : t);
})
.catch(() => {
if (!cancelled) setText("Could not load the file.");
});
return () => {
cancelled = true;
setText(null);
};
}, [file, isText]);
return (
onDownload(file)}>
Download
(onOpenChange(false), onUse(file))}>
Use in new chat
) : undefined
}
>
{file?.kind === "image" ? (
// eslint-disable-next-line @next/next/no-img-element
) : isText ? (
text === null ? (
) : (
{text}
)
) : null}
);
}