From 8a7f6f74dc1c50842b2af3555dd94f0fbfc2ba43 Mon Sep 17 00:00:00 2001 From: Pradeep Date: Mon, 17 Nov 2025 09:06:56 +0530 Subject: [PATCH] newly added multi doc upload service --- src/components/main-dashboard.tsx | 9 +- src/components/multi-docs-upload-inline.tsx | 339 ++++++++++++++++++++ src/components/multi-docs-upload-step.tsx | 258 +++++++++++++++ 3 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 src/components/multi-docs-upload-inline.tsx create mode 100644 src/components/multi-docs-upload-step.tsx diff --git a/src/components/main-dashboard.tsx b/src/components/main-dashboard.tsx index 8cb688d..7567280 100644 --- a/src/components/main-dashboard.tsx +++ b/src/components/main-dashboard.tsx @@ -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 })()} + {/* Multi Docs Upload Inline Section */} +
+ +
+ {templates.length === 0 && !paginationState.loading ? (

No templates found for the current filters.

@@ -2356,6 +2363,7 @@ export function MainDashboard() { // Only access localStorage after component mounts to prevent hydration mismatch return null }) + const [multiDocJobId, setMultiDocJobId] = useState(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" }, diff --git a/src/components/multi-docs-upload-inline.tsx b/src/components/multi-docs-upload-inline.tsx new file mode 100644 index 0000000..5f762ae --- /dev/null +++ b/src/components/multi-docs-upload-inline.tsx @@ -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 = { + 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([]) + const [uploading, setUploading] = useState(false) + const [jobId, setJobId] = useState(null) + const [status, setStatus] = useState(null) + const [error, setError] = useState(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) => { + 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 ( + + setIsExpanded(!isExpanded)} + > +
+
+ + + Upload Documents for Knowledge Graph + +
+
+ {status && status.stage === "completed" && ( + + Completed + + )} + {status && status.stage === "failed" && ( + + Failed + + )} + {isExpanded ? ( + + ) : ( + + )} +
+
+

+ Upload documents to extract causal relationships and build a knowledge graph in Neo4j +

+
+ + {isExpanded && ( + + {status?.stage === "completed" ? ( +
+ + + ✅ Knowledge graph created successfully! {status.total_files} file(s) processed. + + + +
+ ) : status?.stage === "failed" ? ( +
+ + + {error || "Upload failed. Please try again."} + + + +
+ ) : ( + <> +
+ + +
+ + {files.length > 0 && ( +
+

Selected Files ({files.length})

+
    + {files.map(file => ( +
  • + + + {file.name} + + +
  • + ))} +
+
+ )} + + {error && ( + + {error} + + )} + + {status && ( +
+
+ {STAGE_COPY[status.stage] || status.stage} + {progressPercent}% +
+ + {status.status_message && ( +

{status.status_message}

+ )} +
+ )} + + + + )} +
+ )} +
+ ) +} + diff --git a/src/components/multi-docs-upload-step.tsx b/src/components/multi-docs-upload-step.tsx new file mode 100644 index 0000000..8926f05 --- /dev/null +++ b/src/components/multi-docs-upload-step.tsx @@ -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 = { + 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([]) + const [uploading, setUploading] = useState(false) + const [jobId, setJobId] = useState(null) + const [status, setStatus] = useState(null) + const [error, setError] = useState(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) => { + 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 ( +
+
+ + + Multi Docs Upload +

+ Upload multiple documents at once. We'll extract granular context, call Claude Sonnet, and store the causal knowledge graph in Neo4j. +

+
+ +
+ + +
+ + {files.length > 0 && ( +
+

Selected Files ({files.length})

+
    + {files.map(file => ( +
  • + + + {file.name} + + +
  • + ))} +
+
+ )} + + {error && ( + + Upload issue + {error} + + )} + + {status && ( +
+
+ {STAGE_COPY[status.stage] || status.stage} + {progressPercent}% +
+ + {status.status_message && ( +

{status.status_message}

+ )} +
+ )} + +
+ +
+ + +
+
+
+
+
+
+ ) +} +