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}
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+