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
+32 -12
View File
@@ -1,12 +1,12 @@
import { useState } from "react";
import { X } from "lucide-react";
import { AppLayout } from "@/components/layout/app-layout";
import { FilesNav } from "@/components/files/files-nav";
import { FileList } from "@/components/files/file-list";
import { useState } from "react";
import { FileDetails } from "@/components/files/file-details";
import { FileList } from "@/components/files/file-list";
import { FileUploadArea } from "@/components/files/file-upload-area";
import { useFilesPageStore } from "@/stores/files-page-store";
import { FilesNav } from "@/components/files/files-nav";
import { AppLayout } from "@/components/layout/app-layout";
import { useMobile } from "@/hooks/use-mobile";
import { useFilesPageStore } from "@/stores/files-page-store";
export function FilesPage() {
const { activeTab, setActiveTab, selectedFileId } = useFilesPageStore();
@@ -20,6 +20,7 @@ export function FilesPage() {
{/* Mobile tabs */}
<div className="flex border-b border-border">
<button
type="button"
onClick={() => setActiveTab("recent")}
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
activeTab === "recent"
@@ -30,6 +31,7 @@ export function FilesPage() {
Recent
</button>
<button
type="button"
onClick={() => setActiveTab("upload")}
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
activeTab === "upload"
@@ -42,7 +44,17 @@ export function FilesPage() {
</div>
{activeTab === "recent" ? (
<div className="flex-1 overflow-hidden" onClick={() => { if (selectedFileId) setShowDetails(true); }}>
<div
role="listbox"
tabIndex={0}
className="flex-1 overflow-hidden"
onClick={() => {
if (selectedFileId) setShowDetails(true);
}}
onKeyDown={(e) => {
if ((e.key === "Enter" || e.key === " ") && selectedFileId) setShowDetails(true);
}}
>
<FileList />
</div>
) : (
@@ -51,14 +63,22 @@ export function FilesPage() {
{/* Mobile detail bottom sheet */}
{showDetails && selectedFileId && (
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => setShowDetails(false)}>
<div
className="absolute bottom-0 left-0 right-0 bg-background rounded-t-xl p-4 max-h-[70vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div
role="dialog"
aria-modal="true"
aria-label="File Details"
className="fixed inset-0 z-50 bg-black/50"
onClick={(e) => {
if (e.target === e.currentTarget) setShowDetails(false);
}}
onKeyDown={(e) => {
if (e.key === "Escape") setShowDetails(false);
}}
>
<div className="absolute bottom-0 left-0 right-0 bg-background rounded-t-xl p-4 max-h-[70vh] overflow-y-auto">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-semibold">File Details</span>
<button onClick={() => setShowDetails(false)}>
<button type="button" onClick={() => setShowDetails(false)}>
<X className="h-5 w-5 text-muted-foreground" />
</button>
</div>