Oliver-ai-bot_2.0/frontend/components/admin/knowledge-uploader.tsx
Vadym Samoilenko 44a512c41f Phase 1 Complete: Dual-bot architecture, knowledge base, access control
- Remove notebook mode, add RAG + Personal Assistant dual-bot setup
- Add knowledge base management (upload, URL scraping, document processing)
- Add user feature access control (allowed_features, features_override)
- Update admin dashboard with knowledge base tab
- Redesign login page, sidebar, and profile
- Add Celery tasks for async document processing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:40 +00:00

159 lines
5.5 KiB
TypeScript

'use client';
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, FileText, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Progress } from '@/components/ui/progress';
import type { KnowledgeDocumentUploadResponse } from '@/types';
interface KnowledgeUploaderProps {
onUploadComplete: (doc: KnowledgeDocumentUploadResponse) => void;
onUploadError?: (error: string) => void;
}
export function KnowledgeUploader({
onUploadComplete,
onUploadError,
}: KnowledgeUploaderProps) {
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [currentFileName, setCurrentFileName] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const onDrop = useCallback(
async (acceptedFiles: File[]) => {
if (acceptedFiles.length === 0) return;
const file = acceptedFiles[0];
setUploading(true);
setUploadProgress(0);
setCurrentFileName(file.name);
setError(null);
const maxSize = 100 * 1024 * 1024;
if (file.size > maxSize) {
setError('File size exceeds 100MB limit');
setUploading(false);
setCurrentFileName(null);
onUploadError?.('File size exceeds 100MB limit');
return;
}
try {
const formData = new FormData();
formData.append('file', file);
const token = localStorage.getItem('access_token');
setUploadProgress(30);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/admin/knowledge/upload`,
{
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
}
);
if (!response.ok) {
const errorData = await response.json() as Record<string, unknown>;
throw new Error(typeof errorData.detail === 'string' ? errorData.detail : 'Upload failed');
}
const data: KnowledgeDocumentUploadResponse = await response.json();
setUploadProgress(100);
onUploadComplete(data);
setTimeout(() => {
setUploading(false);
setCurrentFileName(null);
setUploadProgress(0);
}, 500);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Failed to upload file';
setError(errorMessage);
onUploadError?.(errorMessage);
setUploading(false);
setCurrentFileName(null);
setUploadProgress(0);
}
},
[onUploadComplete, onUploadError]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
disabled: uploading,
accept: {
'application/pdf': ['.pdf'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
'application/msword': ['.doc'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
'application/vnd.ms-excel': ['.xls'],
'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
'application/vnd.ms-powerpoint': ['.ppt'],
'text/plain': ['.txt'],
'text/csv': ['.csv'],
},
});
return (
<div className="space-y-4">
<div
{...getRootProps()}
className={cn(
'relative cursor-pointer rounded-xl border-2 border-dashed p-8 text-center transition-all',
isDragActive
? 'border-primary bg-primary/5'
: 'border-border bg-background hover:border-muted-foreground/30 hover:bg-muted/50',
uploading && 'pointer-events-none opacity-50'
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center gap-3">
<div className={cn(
'flex h-12 w-12 items-center justify-center rounded-full transition-colors',
isDragActive ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'
)}>
<Upload className="h-6 w-6" />
</div>
<div>
<p className="text-sm font-semibold text-foreground">
{isDragActive ? 'Drop file here' : 'Upload a document'}
</p>
<p className="mt-1 text-xs text-muted-foreground">Drag & drop or click to browse</p>
</div>
<p className="text-xs text-muted-foreground">
PDF, DOCX, DOC, XLSX, XLS, PPTX, PPT, CSV, TXT (Max 100MB)
</p>
</div>
</div>
{uploading && currentFileName && (
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 flex-shrink-0 text-primary" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-foreground">{currentFileName}</p>
<p className="mt-1 text-xs text-muted-foreground">Uploading...</p>
<Progress value={uploadProgress} className="mt-2 h-1.5" />
</div>
</div>
</div>
)}
{error && !uploading && (
<div className="rounded-lg bg-destructive/10 p-4">
<div className="flex items-start gap-3">
<X className="h-5 w-5 flex-shrink-0 text-destructive" />
<div className="flex-1">
<p className="text-sm font-medium text-destructive">Upload Failed</p>
<p className="mt-1 text-sm text-destructive/80">{error}</p>
</div>
</div>
</div>
)}
</div>
);
}