Jai-rathore29's picture
Deploy: DocAgent backend (deterministic date-anomaly fix)
f65e025
Raw
History Blame Contribute Delete
7.37 kB
"use client";
import { useMemo, useRef, useState } from "react";
import {
AlertCircle,
CheckCircle2,
FileText,
Loader2,
Plus,
Search,
Trash2,
UploadCloud,
} from "lucide-react";
import { api } from "@/lib/api";
import { toast } from "@/lib/toast";
import type { DocumentMeta } from "@/lib/types";
function StatusDot({ status }: { status: DocumentMeta["status"] }) {
if (status === "ready")
return <CheckCircle2 size={12} className="text-emerald-400" />;
if (status === "failed")
return <AlertCircle size={12} className="text-rose-400" />;
return <Loader2 size={12} className="animate-spin text-brand-300" />;
}
function timeAgo(iso: string): string {
const d = new Date(iso).getTime();
if (isNaN(d)) return "";
const s = Math.floor((Date.now() - d) / 1000);
if (s < 60) return "just now";
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
return `${Math.floor(s / 86400)}d ago`;
}
export default function Sidebar({
docs,
activeId,
onSelect,
onUploaded,
onDeleted,
}: {
docs: DocumentMeta[];
activeId: string | null;
onSelect: (id: string) => void;
onUploaded: (meta: DocumentMeta) => void;
onDeleted: (id: string) => void;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const [drag, setDrag] = useState(false);
const [q, setQ] = useState("");
const filtered = useMemo(
() =>
docs.filter((d) => d.filename.toLowerCase().includes(q.toLowerCase())),
[docs, q],
);
const doUpload = async (file: File) => {
setUploading(true);
const t = toast.info("Uploading…", file.name);
try {
const meta = await api.upload(file);
onUploaded(meta);
toast.success("Uploaded", `${file.name} is being processed.`);
} catch (e: any) {
toast.error("Upload failed", e?.message ?? String(e));
} finally {
setUploading(false);
}
};
return (
<aside className="flex h-full w-64 flex-col border-r border-white/[0.06] bg-surface-900/60">
{/* upload */}
<div className="p-3">
<button
onClick={() => inputRef.current?.click()}
disabled={uploading}
className="group relative flex w-full items-center justify-center gap-2 overflow-hidden rounded-xl bg-gradient-to-r from-brand-500 to-brand-600 px-3 py-2.5 text-sm font-semibold text-white shadow-glow transition hover:from-brand-400 hover:to-brand-500 disabled:opacity-60"
>
<span className="absolute inset-0 -translate-x-full bg-sheen transition-transform duration-700 group-hover:translate-x-full" />
{uploading ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Plus size={15} />
)}
{uploading ? "Uploading…" : "New document"}
</button>
<input
ref={inputRef}
type="file"
hidden
accept=".pdf,.docx,.doc,.pptx,.xlsx,.html,.md,.txt,.png,.jpg,.jpeg,.tiff,.bmp"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) doUpload(f);
e.target.value = "";
}}
/>
</div>
{/* dropzone */}
<div
onDragOver={(e) => {
e.preventDefault();
setDrag(true);
}}
onDragLeave={() => setDrag(false)}
onDrop={(e) => {
e.preventDefault();
setDrag(false);
const f = e.dataTransfer.files?.[0];
if (f) doUpload(f);
}}
onClick={() => inputRef.current?.click()}
className={`mx-3 mb-3 flex cursor-pointer flex-col items-center gap-1.5 rounded-xl border border-dashed px-3 py-5 text-center transition ${
drag
? "border-brand-400 bg-brand-500/10"
: "border-white/10 hover:border-white/20"
}`}
>
<UploadCloud
size={20}
className={drag ? "text-brand-300" : "text-white/30"}
/>
<p className="text-[11px] leading-tight text-white/40">
Drag & drop or <span className="text-brand-300">browse</span>
</p>
<p className="text-[10px] text-white/25">PDF · DOCX · XLSX · images</p>
</div>
{/* search */}
{docs.length > 4 && (
<div className="mx-3 mb-2">
<div className="flex items-center gap-2 rounded-lg border border-white/[0.06] bg-surface-850 px-2.5 py-1.5">
<Search size={13} className="text-white/30" />
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search documents"
className="w-full bg-transparent text-xs text-white placeholder:text-white/25 focus:outline-none"
/>
</div>
</div>
)}
<div className="flex items-center justify-between px-4 pb-1.5 pt-1">
<span className="text-[10px] font-semibold uppercase tracking-wider text-white/30">
Library
</span>
<span className="rounded-full bg-white/[0.05] px-1.5 text-[10px] font-medium text-white/40">
{docs.length}
</span>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-3">
{filtered.length === 0 && (
<p className="px-2 py-8 text-center text-xs text-white/25">
{docs.length === 0 ? "No documents yet." : "No matches."}
</p>
)}
{filtered.map((d) => (
<div
key={d.id}
onClick={() => onSelect(d.id)}
className={`group mb-1 flex cursor-pointer items-center gap-2.5 rounded-xl px-2.5 py-2 transition ${
activeId === d.id
? "bg-white/[0.06] shadow-[inset_0_0_0_1px_rgba(99,102,241,0.35)]"
: "hover:bg-white/[0.03]"
}`}
>
<div
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-lg ${
activeId === d.id
? "bg-brand-500/20 text-brand-300"
: "bg-white/[0.04] text-white/35"
}`}
>
<FileText size={14} />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-medium text-white/90">
{d.filename}
</p>
<p className="flex items-center gap-1.5 text-[10px] text-white/35">
<StatusDot status={d.status} />
<span className="capitalize">{d.status}</span>
<span className="text-white/15">·</span>
<span>{timeAgo(d.created_at)}</span>
</p>
</div>
<button
onClick={(e) => {
e.stopPropagation();
if (confirm(`Delete "${d.filename}"?`)) onDeleted(d.id);
}}
className="shrink-0 text-white/0 transition group-hover:text-white/30 hover:!text-rose-400"
>
<Trash2 size={13} />
</button>
</div>
))}
</div>
<div className="flex items-center gap-1.5 border-t border-white/[0.06] px-4 py-2.5 text-[10px] text-white/25">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400/70" />
Docling · Gemini · local-first
</div>
</aside>
);
}