- 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>
159 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|