Frontend-Data-Eyond / src /app /components /KnowledgeManagement.tsx
ishaq101's picture
update dockerfile
91cd4b2
import { useState, useEffect } from "react";
import {
Upload,
Trash2,
FileText,
Check,
Loader2,
Database,
X,
} from "lucide-react";
import {
getDocuments,
uploadDocument,
processDocument,
deleteDocument,
type ApiDocument,
type DocumentStatus,
} from "../../services/api";
interface KnowledgeManagementProps {
open: boolean;
onClose: () => void;
}
const getUserId = (): string | null => {
const stored = localStorage.getItem("chatbot_user");
if (!stored) return null;
return (JSON.parse(stored).user_id as string) ?? null;
};
export default function KnowledgeManagement({
open,
onClose,
}: KnowledgeManagementProps) {
const [documents, setDocuments] = useState<ApiDocument[]>([]);
const [loadingDocs, setLoadingDocs] = useState(false);
const [docsError, setDocsError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const [processing, setProcessing] = useState<string | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
const userId = getUserId();
if (!userId) return;
loadDocuments(userId);
}, [open]);
const loadDocuments = async (userId: string) => {
setLoadingDocs(true);
setDocsError(null);
try {
setDocuments(await getDocuments(userId));
} catch (err) {
setDocsError(
err instanceof Error ? err.message : "Failed to load documents"
);
} finally {
setLoadingDocs(false);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const userId = getUserId();
if (!userId) return;
setUploading(true);
setUploadError(null);
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
const uploadRes = await uploadDocument(userId, file);
const newDoc: ApiDocument = {
id: uploadRes.data.id,
filename: uploadRes.data.filename,
status: "pending",
file_size: file.size,
file_type: file.name.split(".").pop() ?? "",
created_at: new Date().toISOString(),
};
setDocuments((prev) => [newDoc, ...prev]);
await processDocumentById(userId, uploadRes.data.id);
} catch (err) {
setUploadError(err instanceof Error ? err.message : "Upload failed");
}
}
setUploading(false);
e.target.value = "";
};
const processDocumentById = async (userId: string, docId: string) => {
setProcessing(docId);
setDocuments((prev) =>
prev.map((d) =>
d.id === docId ? { ...d, status: "processing" as DocumentStatus } : d
)
);
try {
await processDocument(userId, docId);
setDocuments((prev) =>
prev.map((d) =>
d.id === docId ? { ...d, status: "completed" as DocumentStatus } : d
)
);
} catch {
setDocuments((prev) =>
prev.map((d) =>
d.id === docId ? { ...d, status: "failed" as DocumentStatus } : d
)
);
} finally {
setProcessing(null);
}
};
const handleDeleteDocument = async (docId: string) => {
const userId = getUserId();
if (!userId) return;
setDeleting(docId);
try {
await deleteDocument(userId, docId);
setDocuments((prev) => prev.filter((d) => d.id !== docId));
} catch (err) {
console.error("Delete failed:", err);
} finally {
setDeleting(null);
}
};
const deleteAllDocuments = async () => {
if (!window.confirm("Are you sure you want to delete all documents?"))
return;
const userId = getUserId();
if (!userId) return;
for (const doc of documents) {
try {
await deleteDocument(userId, doc.id);
} catch {
// continue deleting others
}
}
setDocuments([]);
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
return (bytes / (1024 * 1024)).toFixed(2) + " MB";
};
const formatDate = (isoString: string) => {
return new Date(isoString).toLocaleString();
};
const renderStatus = (doc: ApiDocument) => {
if (doc.status === "completed") {
return (
<div className="flex items-center gap-1.5 text-green-600">
<Check className="w-3.5 h-3.5" />
<span className="text-xs font-medium">Processed</span>
</div>
);
}
if (doc.status === "processing" || processing === doc.id) {
return (
<div className="flex items-center gap-1.5 text-blue-600">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span className="text-xs">Processing...</span>
</div>
);
}
// pending or failed
return (
<button
onClick={() => {
const userId = getUserId();
if (userId) processDocumentById(userId, doc.id);
}}
disabled={processing === doc.id}
className="flex items-center gap-1.5 bg-gradient-to-r from-[#00C853] to-[#00A843] text-white px-3 py-1.5 rounded-lg hover:from-[#00A843] hover:to-[#00962B] transition disabled:opacity-50 disabled:cursor-not-allowed text-xs"
>
<Database className="w-3.5 h-3.5" />
{doc.status === "failed" ? "Retry Process" : "Process to Knowledge"}
</button>
);
};
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col m-4">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-200 bg-gradient-to-r from-[#FF8F00] to-[#FF6F00]">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-white" />
<h2 className="text-lg text-white">Knowledge Management</h2>
</div>
<button
onClick={onClose}
className="text-white/80 hover:text-white transition"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{/* Upload Section */}
<div className="mb-4">
<label
htmlFor="file-upload"
className="flex items-center justify-center gap-2 border-2 border-dashed border-slate-300 rounded-lg p-6 cursor-pointer hover:border-[#4FC3F7] hover:bg-slate-50 transition"
>
<Upload className="w-5 h-5 text-slate-600" />
<div className="text-center">
<p className="text-slate-900 font-medium text-sm">
Upload Documents (PDF, DOCX, TXT)
</p>
<p className="text-xs text-slate-500 mt-0.5">
Click to browse or drag and drop
</p>
</div>
<input
id="file-upload"
type="file"
accept=".pdf,.docx,.txt"
multiple
onChange={handleFileUpload}
className="hidden"
disabled={uploading}
/>
</label>
{uploadError && (
<p className="mt-2 text-xs text-red-600 bg-red-50 border border-red-200 px-3 py-2 rounded-lg">
{uploadError}
</p>
)}
</div>
{/* Documents List */}
<div className="space-y-2">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm text-slate-900 font-medium">
Documents ({documents.length})
</h3>
{documents.length > 0 && (
<button
onClick={deleteAllDocuments}
className="text-xs text-red-600 hover:text-red-700 flex items-center gap-1"
>
<Trash2 className="w-3.5 h-3.5" />
Delete All
</button>
)}
</div>
{loadingDocs ? (
<div className="flex justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-slate-400" />
</div>
) : docsError ? (
<p className="text-center text-sm text-red-600 py-4">
{docsError}
</p>
) : documents.length === 0 ? (
<div className="text-center py-8">
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500 text-sm">
No documents uploaded yet
</p>
<p className="text-xs text-slate-400 mt-1">
Upload files to build your knowledge base
</p>
</div>
) : (
documents.map((doc) => (
<div
key={doc.id}
className="bg-slate-50 rounded-lg p-3 border border-slate-200"
>
<div className="flex items-start gap-3">
<FileText className="w-8 h-8 text-red-500 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h4 className="text-slate-900 font-medium truncate text-sm">
{doc.filename}
</h4>
<p className="text-xs text-slate-500 mt-0.5">
{formatFileSize(doc.file_size)} •{" "}
{formatDate(doc.created_at)}
</p>
</div>
<button
onClick={() => handleDeleteDocument(doc.id)}
disabled={deleting === doc.id}
className="text-slate-400 hover:text-red-600 transition flex-shrink-0 disabled:opacity-50"
title="Delete document"
>
{deleting === doc.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
<div className="mt-2 flex items-center gap-2">
{renderStatus(doc)}
</div>
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Footer */}
<div className="border-t border-slate-200 p-3 bg-slate-50 rounded-b-xl">
<p className="text-[10px] text-slate-500 text-center">
Supported formats: PDF, DOCX, TXT
</p>
</div>
</div>
</div>
);
}