newly added multi doc upload service

This commit is contained in:
Pradeep 2025-11-17 09:06:56 +05:30
parent de9a6e9eb7
commit 8a7f6f74dc
3 changed files with 605 additions and 1 deletions

View File

@ -28,6 +28,8 @@ import ViewUserReposButton from "@/components/github/ViewUserReposButton"
import { ErrorBanner } from "@/components/ui/error-banner"
import { useAuth } from "@/contexts/auth-context"
import { authApiClient } from "@/components/apis/authApiClients"
import { MultiDocsUploadInline } from "@/components/multi-docs-upload-inline"
import { MultiDocsUploadStep } from "@/components/multi-docs-upload-step"
interface Template {
id: string
@ -1143,6 +1145,11 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
})()}
</div>
{/* Multi Docs Upload Inline Section */}
<div className="max-w-4xl mx-auto">
<MultiDocsUploadInline />
</div>
{templates.length === 0 && !paginationState.loading ? (
<div className="text-center py-12 text-white/60">
<p>No templates found for the current filters.</p>
@ -2356,6 +2363,7 @@ export function MainDashboard() {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
const [multiDocJobId, setMultiDocJobId] = useState<string | null>(null)
// Load state from localStorage after component mounts
useEffect(() => {
@ -2413,7 +2421,6 @@ export function MainDashboard() {
const steps = [
{ id: 1, name: "Project Type", description: "Choose template" },
{ id: 2, name: "Features", description: "Select features" },
// { id: 3, name: "AI Mockup", description: "Generate wireframes" },
{ id: 3, name: "Business Context", description: "Define requirements" },
{ id: 4, name: "Generate", description: "Create project" },
{ id: 5, name: "Architecture", description: "Review & deploy" },

View File

@ -0,0 +1,339 @@
"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>
)
}

View File

@ -0,0 +1,258 @@
"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, AlertTitle } from "@/components/ui/alert"
import { Upload, File as FileIcon, Loader2, ArrowLeft, ArrowRight } from "lucide-react"
import { getApiUrl } from "@/config/backend"
interface MultiDocsUploadStepProps {
onBack: () => void
onNext: (jobId: string | null) => void
}
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",
}
export function MultiDocsUploadStep({ onBack, onNext }: MultiDocsUploadStepProps) {
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") {
onNext(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, onNext])
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)
} catch (err: any) {
console.error("Upload failed:", err)
setError(err.message ?? "Upload failed. Please try again.")
setUploading(false)
}
}
const progressPercent = status
? status.total_files > 0
? Math.round((status.processed_files / status.total_files) * 100)
: 0
: 0
return (
<div className="min-h-[70vh] bg-gradient-to-b from-black to-black/80 text-white">
<div className="max-w-5xl mx-auto py-12 px-6">
<Card className="bg-white/5 border border-white/10">
<CardHeader>
<CardTitle className="text-2xl text-orange-400">Multi Docs Upload</CardTitle>
<p className="text-sm text-white/60">
Upload multiple documents at once. We&apos;ll extract granular context, call Claude Sonnet, and store the causal knowledge graph in Neo4j.
</p>
</CardHeader>
<CardContent className="space-y-6">
<div className="border border-dashed border-orange-400/40 rounded-lg p-6 text-center bg-black/40">
<input
id="multi-docs-input"
type="file"
multiple
onChange={handleFileSelect}
className="hidden"
/>
<label htmlFor="multi-docs-input" className="flex flex-col items-center cursor-pointer space-y-3">
<Upload className="h-10 w-10 text-orange-400" />
<div>
<p className="font-semibold text-white">Click to select files</p>
<p className="text-sm text-white/60">
Supports PDF, DOCX, PPT, XLS, JSON, XML, Markdown, images, audio/video and more. You can mix formats.
</p>
</div>
<span className="text-xs uppercase tracking-wide text-orange-300 bg-orange-400/10 px-3 py-1 rounded-full">
Claude model: Sonnet 20241022 (High-accuracy causal extraction)
</span>
</label>
</div>
{files.length > 0 && (
<div className="bg-black/40 border border-white/10 rounded-lg p-4 space-y-3">
<p className="text-sm font-semibold text-white/70">Selected Files ({files.length})</p>
<ul className="space-y-2 max-h-48 overflow-y-auto">
{files.map(file => (
<li key={file.name} className="flex items-center justify-between text-sm text-white/80 bg-white/5 rounded-md px-3 py-2">
<span className="flex items-center gap-2">
<FileIcon className="h-4 w-4 text-orange-400" />
{file.name}
</span>
<button
type="button"
className="text-orange-300 hover:text-orange-200 text-xs"
onClick={() => removeFile(file.name)}
disabled={uploading}
>
Remove
</button>
</li>
))}
</ul>
</div>
)}
{error && (
<Alert variant="destructive" className="bg-red-500/10 border border-red-500/40 text-red-200">
<AlertTitle>Upload issue</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{status && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm text-white/70">
<span>{STAGE_COPY[status.stage] || status.stage}</span>
<span>{progressPercent}%</span>
</div>
<Progress value={progressPercent} className="h-2" />
{status.status_message && (
<p className="text-xs text-white/60">{status.status_message}</p>
)}
</div>
)}
<div className="flex justify-between pt-6">
<Button
variant="outline"
onClick={onBack}
className="bg-transparent border border-white/20 text-white hover:bg-white/10"
disabled={uploading}
>
<ArrowLeft className="mr-2 h-4 w-4" /> Back
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => onNext(jobId)}
className="bg-transparent border border-white/20 text-white hover:bg-white/10"
disabled={!jobId || uploading}
>
Skip
</Button>
<Button
onClick={startUpload}
className="bg-orange-500 hover:bg-orange-600 text-black font-semibold"
disabled={uploading}
>
{uploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Uploading...
</>
) : (
<>
Start Upload <ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}