// Copyright: © 2026 TWWIM UG. All rights reserved. (www.twwim.com) import { useRef, useState } from 'react'; import { FileText, Trash2, Plus, AlertCircle, Database, ShieldAlert, Upload, X, Download, Loader2, CheckCircle2, XCircle, } from 'lucide-react'; import { useKnowledgeDocuments, useUploadKnowledgeDocument, useDeleteKnowledgeDocument, } from '../hooks'; import { useTranslation } from '@/i18n/TranslationProvider'; import { tokenStorage } from '@/infrastructure/storage/LocalTokenStorage'; import type { KnowledgeDocument } from '@/infrastructure/http/api/knowledge'; import { knowledgeApi } from '@/infrastructure/http/api/knowledge'; import { apiClient } from '@/infrastructure/http/ApiClient'; const ALLOWED_EXTENSIONS = '.pdf,.docx,.txt'; const ALLOWED_MIME = [ 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', ]; const FILE_BYTES_MAX = 25 * 1024 * 1024; function formatBytes(b: number): string { if (b < 1024) return `${b} B`; if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; return `${(b / 1024 / 1024).toFixed(2)} MB`; } function StatusPill({ status }: { status: KnowledgeDocument['status'] }) { const map: Record = { PENDING: { color: 'bg-gray-100 text-gray-700', icon: , label: 'Pending' }, PROCESSING: { color: 'bg-blue-100 text-blue-700', icon: , label: 'Processing' }, COMPLETED: { color: 'bg-emerald-100 text-emerald-800', icon: , label: 'Indexed' }, FAILED: { color: 'bg-red-100 text-red-800', icon: , label: 'Failed' }, }; const cur = map[status] ?? map.PENDING; return ( {cur.icon} {cur.label} ); } function UploadForm({ tenantId, onClose }: { tenantId: string; onClose: () => void }) { const { t } = useTranslation(); const upload = useUploadKnowledgeDocument(tenantId); const [file, setFile] = useState(null); const [localError, setLocalError] = useState(null); const inputRef = useRef(null); const pick = (chosen: File | null) => { setLocalError(null); if (!chosen) { setFile(null); return; } if (!ALLOWED_MIME.includes(chosen.type)) { setLocalError(`Unsupported type: ${chosen.type || 'unknown'}. Allowed: PDF, DOCX, TXT.`); return; } if (chosen.size > FILE_BYTES_MAX) { setLocalError(`File too large (${formatBytes(chosen.size)}). Max ${formatBytes(FILE_BYTES_MAX)}.`); return; } setFile(chosen); }; const submit = (e: React.FormEvent) => { e.preventDefault(); if (!file) return; upload.mutate(file, { onSuccess: () => onClose(), }); }; return (

{t('knowledge.uploadDocument') ?? 'Upload document'}

pick(e.target.files?.[0] ?? null)} className="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-medium file:bg-cyan-50 file:text-cyan-700 hover:file:bg-cyan-100" /> {file && (
{file.name} {formatBytes(file.size)}
)} {localError && (
{localError}
)} {upload.isError && !localError && (
{upload.error?.message ?? (t('knowledge.uploadError') ?? 'Upload failed')}
)}
); } function DocumentRow({ doc, tenantId }: { doc: KnowledgeDocument; tenantId: string }) { const { t } = useTranslation(); const del = useDeleteKnowledgeDocument(tenantId); const [confirm, setConfirm] = useState(false); const download = async () => { // Hit the authenticated endpoint via ApiClient (carries the JWT), then // turn the blob into a temporary click. try { const resp = await (apiClient as any).get( knowledgeApi.getDocumentDownloadUrl(tenantId, doc.id), { responseType: 'blob' }, ); const blob = resp as Blob; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = doc.filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (e) { // Surface a non-blocking error console.error('Download failed', e); } }; return (
{doc.filename}
{doc.mimeType} · {formatBytes(doc.sizeBytes)} {doc.chunkCount > 0 && · {doc.chunkCount} chunks}
{doc.errorMessage && (
{doc.errorMessage}
)}
{new Date(doc.createdAt).toLocaleDateString()}
{confirm ? ( <> ) : ( )}
); } interface KnowledgeDocumentsTabProps { tenantId: string; } export function KnowledgeDocumentsTab({ tenantId }: KnowledgeDocumentsTabProps) { const { t } = useTranslation(); const [isAdding, setIsAdding] = useState(false); const { data, isLoading, error } = useKnowledgeDocuments(tenantId); const capValue = tokenStorage.getCapability('knowledge_files_max'); const maxFiles = capValue ? parseInt(capValue, 10) : 0; const featureEnabled = maxFiles > 0; const totalDocs = data?.total ?? 0; const atLimit = !featureEnabled || totalDocs >= maxFiles; if (isLoading) { return (
); } if (error) { return (
{t('knowledge.loadError')}: {error.message}
); } const items = data?.items ?? []; return (
{!featureEnabled && (
{t('knowledge.featureDisabled')}
)} {featureEnabled && totalDocs >= maxFiles && (
{t('knowledge.limitReached') ?? `Document limit reached (${totalDocs}/${maxFiles}).`}
)}
{totalDocs} / {maxFiles} {totalDocs === 1 ? 'document' : 'documents'}
{isAdding && !atLimit && (
setIsAdding(false)} />
)} {items.length === 0 ? (

{t('knowledge.noDocuments') ?? 'No documents yet'}

{t('knowledge.noDocumentsHint') ?? 'Upload a PDF, DOCX, or TXT file to get started.'}

) : (
{items.map((doc: KnowledgeDocument) => ( ))}
{t('knowledge.title')} {t('knowledge.status')} {t('knowledge.created')} {t('common.actions')}
)}
); }