codenuk_frontend_mine/src/components/multi-docs-upload-inline.tsx
2025-11-17 09:06:56 +05:30

340 lines
12 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Upload, File as FileIcon, Loader2, ChevronDown, ChevronUp, X } from "lucide-react"
import { getApiUrl } from "@/config/backend"
interface JobStatus {
job_id: string
stage: string
status_message?: string | null
processed_files: number
total_files: number
error?: string | null
}
const STAGE_COPY: Record<string, string> = {
received: "Job received",
saving_files: "Saving files",
extracting: "Extracting document content",
analyzing: "Calling Claude for causal relations",
building_graph: "Writing to Neo4j knowledge graph",
completed: "Completed",
failed: "Failed",
}
interface MultiDocsUploadInlineProps {
onJobCreated?: (jobId: string) => void
}
// Default export for easier importing
export default MultiDocsUploadInline
export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlineProps) {
const [isExpanded, setIsExpanded] = useState(false)
const [files, setFiles] = useState<File[]>([])
const [uploading, setUploading] = useState(false)
const [jobId, setJobId] = useState<string | null>(null)
const [status, setStatus] = useState<JobStatus | null>(null)
const [error, setError] = useState<string | null>(null)
const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), [])
useEffect(() => {
if (!jobId) return
let cancelled = false
const interval = setInterval(async () => {
try {
const response = await fetch(getApiUrl(`/api/multi-docs/jobs/${jobId}`))
if (!response.ok) {
throw new Error(`Status request failed with ${response.status}`)
}
const data = (await response.json()) as JobStatus
if (!cancelled) {
setStatus(data)
if (data.stage === "completed" || data.stage === "failed") {
clearInterval(interval)
if (data.stage === "completed" && onJobCreated) {
onJobCreated(jobId)
}
}
}
} catch (err: any) {
if (!cancelled) {
console.error("Failed to poll job status:", err)
setError(err.message ?? "Failed to fetch job status")
clearInterval(interval)
}
}
}, 4000)
return () => {
cancelled = true
clearInterval(interval)
}
}, [jobId, onJobCreated])
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.files) return
const selected = Array.from(event.target.files)
setFiles(prev => [...prev, ...selected])
}
const removeFile = (fileName: string) => {
setFiles(prev => prev.filter(file => file.name !== fileName))
}
const startUpload = async () => {
setError(null)
if (files.length === 0) {
setError("Please select at least one file.")
return
}
const formData = new FormData()
files.forEach(file => formData.append("files", file))
setUploading(true)
try {
const response = await fetch(uploadUrl, {
method: "POST",
body: formData,
})
if (!response.ok) {
let errorMessage = `Upload failed with ${response.status}`
try {
const errorData = await response.json()
errorMessage = errorData.detail || errorData.message || errorMessage
} catch {
const text = await response.text()
errorMessage = text || errorMessage
}
throw new Error(errorMessage)
}
const data = await response.json()
setJobId(data.job_id)
setStatus({
job_id: data.job_id,
stage: data.stage,
total_files: data.total_files,
processed_files: 0,
})
setUploading(false)
setIsExpanded(true) // Keep expanded when upload starts
} catch (err: any) {
console.error("Upload failed:", err)
setError(err.message ?? "Upload failed. Please try again.")
setUploading(false)
}
}
// Calculate progress based on stage and file processing
const progressPercent = status
? (() => {
// File extraction progress (0-30%)
const fileProgress = status.total_files > 0
? Math.round((status.processed_files / status.total_files) * 30)
: 0
// Stage-based progress
let stageProgress = 0
if (status.stage === "saving_files") {
stageProgress = 5
} else if (status.stage === "extracting") {
stageProgress = 10 + fileProgress // 10-40%
} else if (status.stage === "analyzing") {
// Analyzing takes the longest, show 40-85%
stageProgress = 40 + Math.min(fileProgress * 1.5, 45) // 40-85%
} else if (status.stage === "building_graph") {
stageProgress = 85 + Math.min(fileProgress * 0.5, 10) // 85-95%
} else if (status.stage === "completed") {
stageProgress = 100
} else if (status.stage === "failed") {
stageProgress = fileProgress // Show progress up to failure point
}
return Math.min(stageProgress, 100)
})()
: 0
const resetUpload = () => {
setFiles([])
setJobId(null)
setStatus(null)
setError(null)
setUploading(false)
}
return (
<Card className="bg-white/5 border border-white/10 rounded-xl">
<CardHeader
className="cursor-pointer hover:bg-white/5 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Upload className="h-5 w-5 text-orange-400" />
<CardTitle className="text-lg text-white">
Upload Documents for Knowledge Graph
</CardTitle>
</div>
<div className="flex items-center space-x-2">
{status && status.stage === "completed" && (
<span className="text-xs text-green-400 bg-green-400/10 px-2 py-1 rounded-full">
Completed
</span>
)}
{status && status.stage === "failed" && (
<span className="text-xs text-red-400 bg-red-400/10 px-2 py-1 rounded-full">
Failed
</span>
)}
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-white/60" />
) : (
<ChevronDown className="h-5 w-5 text-white/60" />
)}
</div>
</div>
<p className="text-sm text-white/60 mt-2">
Upload documents to extract causal relationships and build a knowledge graph in Neo4j
</p>
</CardHeader>
{isExpanded && (
<CardContent className="space-y-4">
{status?.stage === "completed" ? (
<div className="space-y-3">
<Alert className="bg-green-500/10 border border-green-500/40 text-green-200">
<AlertDescription>
Knowledge graph created successfully! {status.total_files} file(s) processed.
</AlertDescription>
</Alert>
<Button
onClick={resetUpload}
variant="outline"
className="w-full border-white/20 text-white hover:bg-white/10"
>
Upload More Documents
</Button>
</div>
) : status?.stage === "failed" ? (
<div className="space-y-3">
<Alert variant="destructive" className="bg-red-500/10 border border-red-500/40 text-red-200">
<AlertDescription>
{error || "Upload failed. Please try again."}
</AlertDescription>
</Alert>
<Button
onClick={resetUpload}
variant="outline"
className="w-full border-white/20 text-white hover:bg-white/10"
>
Try Again
</Button>
</div>
) : (
<>
<div className="border border-dashed border-orange-400/40 rounded-lg p-4 text-center bg-black/40">
<input
id="multi-docs-input-inline"
type="file"
multiple
onChange={handleFileSelect}
className="hidden"
disabled={uploading}
/>
<label
htmlFor="multi-docs-input-inline"
className={`flex flex-col items-center cursor-pointer space-y-2 ${uploading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<Upload className="h-8 w-8 text-orange-400" />
<div>
<p className="font-semibold text-white text-sm">Click to select files</p>
<p className="text-xs text-white/60">
PDF, DOCX, PPT, XLS, JSON, XML, Markdown, images, audio/video
</p>
</div>
</label>
</div>
{files.length > 0 && (
<div className="bg-black/40 border border-white/10 rounded-lg p-3 space-y-2">
<p className="text-xs font-semibold text-white/70">Selected Files ({files.length})</p>
<ul className="space-y-1 max-h-32 overflow-y-auto">
{files.map(file => (
<li key={file.name} className="flex items-center justify-between text-xs text-white/80 bg-white/5 rounded px-2 py-1">
<span className="flex items-center gap-2">
<FileIcon className="h-3 w-3 text-orange-400" />
<span className="truncate max-w-[200px]">{file.name}</span>
</span>
<button
type="button"
className="text-orange-300 hover:text-orange-200"
onClick={(e) => {
e.stopPropagation()
removeFile(file.name)
}}
disabled={uploading}
>
<X className="h-3 w-3" />
</button>
</li>
))}
</ul>
</div>
)}
{error && (
<Alert variant="destructive" className="bg-red-500/10 border border-red-500/40 text-red-200">
<AlertDescription className="text-xs">{error}</AlertDescription>
</Alert>
)}
{status && (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs text-white/70">
<span>{STAGE_COPY[status.stage] || status.stage}</span>
<span>{progressPercent}%</span>
</div>
<Progress value={progressPercent} className="h-1.5" />
{status.status_message && (
<p className="text-xs text-white/60">{status.status_message}</p>
)}
</div>
)}
<Button
onClick={(e) => {
e.stopPropagation()
startUpload()
}}
className="w-full bg-orange-500 hover:bg-orange-600 text-black font-semibold"
disabled={uploading || files.length === 0}
>
{uploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Uploading...
</>
) : (
<>
Start Upload <Upload className="ml-2 h-4 w-4" />
</>
)}
</Button>
</>
)}
</CardContent>
)}
</Card>
)
}