feat: implement Files page with persistent storage and version tracking

Three-panel file manager (nav, list, details) modeled after Stirling-PDF.
- Backend: user_files table, /api/v1/files/* CRUD routes, file storage
  helpers, thumbnail generation via Sharp, recursive CTE version chains
- Frontend: FilesNav, FileList, FileDetails, FileUploadArea components,
  Zustand store, mobile layout with bottom sheet
- Integration: tool-factory auto-saves results as new versions when
  fileId is provided, "Open File" loads file into tool processing flow
- Search, bulk select/delete/download, version badges, tool chain tags
This commit is contained in:
Siddharth Kumar Sah
2026-03-26 01:10:51 +08:00
parent 926b52d330
commit 62fbb5484c
24 changed files with 771 additions and 470 deletions
+8 -10
View File
@@ -59,14 +59,12 @@ export function clearToken() {
// ── File Upload / Download ──────────────────────────────────────
export async function apiUpload(
files: File[],
): Promise<{
export async function apiUpload(files: File[]): Promise<{
jobId: string;
files: Array<{ name: string; size: number; format: string }>;
}> {
const formData = new FormData();
files.forEach((f) => formData.append("files", f));
for (const f of files) formData.append("files", f);
const res = await fetch("/api/v1/upload", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
@@ -118,14 +116,17 @@ export async function apiListFiles(params?: {
}
export async function apiGetFileDetails(id: string): Promise<UserFileDetail> {
return apiGet(`/v1/files/${id}`);
const res = await apiGet<{ file: UserFile; versions: UserFileDetail["versions"] }>(
`/v1/files/${id}`,
);
return { ...res.file, versions: res.versions };
}
export async function apiUploadUserFiles(
files: File[],
): Promise<{ files: Array<{ id: string; originalName: string; size: number; version: number }> }> {
const formData = new FormData();
files.forEach((f) => formData.append("files", f));
for (const f of files) formData.append("files", f);
const res = await fetch("/api/v1/files/upload", {
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
@@ -156,10 +157,7 @@ export function getFileDownloadUrl(id: string): string {
return `/api/v1/files/${id}/download`;
}
export async function apiDownloadBlob(
jobId: string,
filename: string,
): Promise<Blob> {
export async function apiDownloadBlob(jobId: string, filename: string): Promise<Blob> {
const res = await fetch(getDownloadUrl(jobId, filename), {
headers: { Authorization: `Bearer ${getToken()}` },
});