diff --git a/src/app/api/multi-docs/jobs/[jobId]/report/pdf/route.ts b/src/app/api/multi-docs/jobs/[jobId]/report/pdf/route.ts new file mode 100644 index 0000000..ca2191b --- /dev/null +++ b/src/app/api/multi-docs/jobs/[jobId]/report/pdf/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getApiUrl } from '@/config/backend'; + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ jobId: string }> } +) { + try { + const { jobId } = await params; + + if (!jobId) { + return NextResponse.json( + { error: 'Job ID is required' }, + { status: 400 } + ); + } + + // Forward request to backend multi-document-upload-service via API gateway + const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL || 'http://localhost:8000' + const fullUrl = `${apiGatewayUrl}/multi-document-upload-service/jobs/${jobId}/report/pdf`; + + const response = await fetch(fullUrl, { + method: 'GET', + }); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: errorText || 'Failed to fetch PDF report' }, + { status: response.status } + ); + } + + // Get PDF as blob + const pdfBlob = await response.blob(); + + // Return PDF with proper headers + return new NextResponse(pdfBlob, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="onboarding_report_${jobId}.pdf"`, + }, + }); + } catch (error: any) { + console.error('Error fetching PDF report:', error); + return NextResponse.json( + { error: error.message || 'Internal server error' }, + { status: 500 } + ); + } +} + diff --git a/src/app/api/multi-docs/jobs/[jobId]/report/route.ts b/src/app/api/multi-docs/jobs/[jobId]/report/route.ts new file mode 100644 index 0000000..daa8810 --- /dev/null +++ b/src/app/api/multi-docs/jobs/[jobId]/report/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getApiUrl } from '@/config/backend'; + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ jobId: string }> } +) { + try { + const { jobId } = await params; + + if (!jobId) { + return NextResponse.json( + { error: 'Job ID is required' }, + { status: 400 } + ); + } + + // Forward request to backend multi-document-upload-service via API gateway + // The API gateway routes /multi-document-upload-service/* to the service + const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL || 'http://localhost:8000' + const fullUrl = `${apiGatewayUrl}/multi-document-upload-service/jobs/${jobId}/report`; + + const response = await fetch(fullUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + return NextResponse.json( + { error: errorText || 'Failed to fetch report' }, + { status: response.status } + ); + } + + const report = await response.json(); + return NextResponse.json(report); + } catch (error: any) { + console.error('Error fetching report:', error); + return NextResponse.json( + { error: error.message || 'Internal server error' }, + { status: 500 } + ); + } +} + diff --git a/src/components/main-dashboard.tsx b/src/components/main-dashboard.tsx index 7567280..35a5f03 100644 --- a/src/components/main-dashboard.tsx +++ b/src/components/main-dashboard.tsx @@ -94,14 +94,27 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi pollCount++; try { - // Poll for repository sync status - const response = await authApiClient.get(`/api/vcs/${provider}/repositories?t=${Date.now()}`, { - headers: { 'x-user-id': user?.id } + // Poll for repository sync status from all_repositories table + // Use the endpoint that queries the database, not the GitHub API + const userId = user?.id; + if (!userId) { + console.error('No user ID available for monitoring'); + clearInterval(pollInterval); + return; + } + + const response = await authApiClient.get(`/api/github/user/${userId}/repositories?t=${Date.now()}&force_refresh=true`, { + headers: { 'x-user-id': userId } }); if (response.data?.success) { const repositories = response.data.data || []; - const repo = repositories.find((r: any) => r.repository_url === repositoryUrl); + const repo = repositories.find((r: any) => + r.repository_url === repositoryUrl || + r.repository_url === decodeURIComponent(repositoryUrl) || + (r.owner_name && r.repository_name && + repositoryUrl.includes(`${r.owner_name}/${r.repository_name}`)) + ); if (repo) { const status = repo.sync_status; @@ -741,11 +754,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi console.log('🔍 Full error object:', err) console.log('🔍 Error response object:', err?.response) - // If backend signals auth required, show authentication button instead of auto-redirect - if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) { - console.log('🔐 Private repository detected, showing authentication button:', { status, requires_auth: data?.requires_auth, message: data?.message }) + // If backend signals auth required, handle OAuth redirect + // Check for requires_auth in response (status can be 200, 401, or any status) + if (data?.requires_auth || data?.message?.includes('authentication') || data?.message?.includes('authentication required')) { + console.log('🔐 Private repository detected, auto-redirecting to OAuth:', { status, requires_auth: data?.requires_auth, message: data?.message, auth_url: data?.auth_url }) - // Store the auth_url for when user clicks the authenticate button + // Store the auth_url for OAuth redirect try { sessionStorage.setItem('pending_git_attach', JSON.stringify({ repository_url: gitUrl.trim(), @@ -758,8 +772,14 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi console.warn('⚠️ Failed to store pending git attach:', e) } - // Don't auto-redirect, let the user click the authenticate button - console.log('🔐 Private repository detected - user must click authenticate button') + // Auto-redirect to OAuth + if (data?.auth_url) { + console.log('🔐 Private repository detected - auto-redirecting to OAuth:', data.auth_url) + window.location.replace(data.auth_url) + return + } else { + alert('Authentication required but OAuth URL not available. Please try again.') + } return } diff --git a/src/components/multi-docs-report-viewer.tsx b/src/components/multi-docs-report-viewer.tsx new file mode 100644 index 0000000..3f9527f --- /dev/null +++ b/src/components/multi-docs-report-viewer.tsx @@ -0,0 +1,238 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Download, FileText, Loader2, X } from "lucide-react" +import { getApiUrl } from "@/config/backend" + +interface ProjectReport { + job_id: string + title: string + content: string + sections: Record + key_concepts: string[] + total_pages: number + generated_at: string + metadata: { + pdf_path?: string + total_relations?: number + total_concepts?: number + [key: string]: any + } +} + +interface MultiDocsReportViewerProps { + jobId: string + onClose?: () => void +} + +export function MultiDocsReportViewer({ jobId, onClose }: MultiDocsReportViewerProps) { + const [report, setReport] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [downloading, setDownloading] = useState(false) + + useEffect(() => { + if (!jobId) return + + const fetchReport = async () => { + setLoading(true) + setError(null) + try { + const response = await fetch(getApiUrl(`/api/multi-docs/jobs/${jobId}/report`)) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ detail: response.statusText })) + // Extract error message from API response + const errorMsg = errorData.detail || errorData.error || `Failed to fetch report: ${response.status}` + throw new Error(errorMsg) + } + + const data = await response.json() + setReport(data) + } catch (err: any) { + console.error("Failed to fetch report:", err) + setError(err.message || "Failed to load report") + } finally { + setLoading(false) + } + } + + fetchReport() + }, [jobId]) + + const handleDownloadPDF = async () => { + if (!jobId) return + + setDownloading(true) + try { + const response = await fetch(getApiUrl(`/api/multi-docs/jobs/${jobId}/report/pdf`)) + + if (!response.ok) { + throw new Error("Failed to download PDF") + } + + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = `onboarding_report_${jobId}.pdf` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + } catch (err: any) { + console.error("Failed to download PDF:", err) + setError(err.message || "Failed to download PDF") + } finally { + setDownloading(false) + } + } + + if (loading) { + return ( + + + +

Loading report...

+
+
+ ) + } + + if (error) { + return ( + + + + {error} + + {onClose && ( + + )} + + + ) + } + + if (!report) { + return ( + + +

No report available

+ {onClose && ( + + )} +
+
+ ) + } + + return ( + + +
+
+ + {report.title} +
+
+ + {onClose && ( + + )} +
+
+
+ {report.total_pages} pages + {report.metadata?.total_relations && ( + {report.metadata.total_relations} relationships + )} + {report.metadata?.total_concepts && ( + {report.metadata.total_concepts} concepts + )} + + Generated: {new Date(report.generated_at).toLocaleDateString()} + +
+
+ +
+
+ {report.content.split('\n').map((line, idx) => { + // Headers + if (line.match(/^### /)) { + return

{line.replace(/^### /, '')}

+ } + if (line.match(/^## /)) { + return

{line.replace(/^## /, '')}

+ } + if (line.match(/^# /)) { + return

{line.replace(/^# /, '')}

+ } + // Lists + if (line.match(/^[\-\*] /)) { + return
  • {line.replace(/^[\-\*] /, '')}
  • + } + if (line.match(/^\d+\. /)) { + return
  • {line.replace(/^\d+\. /, '')}
  • + } + // Code blocks + if (line.match(/^```/)) { + return
    + } + // Regular paragraphs + if (line.trim() === '') { + return
    + } + return

    {line}

    + })} +
    +
    +
    +
    + ) +} + diff --git a/src/components/multi-docs-upload-inline.tsx b/src/components/multi-docs-upload-inline.tsx index 5f762ae..2a6eddb 100644 --- a/src/components/multi-docs-upload-inline.tsx +++ b/src/components/multi-docs-upload-inline.tsx @@ -5,8 +5,9 @@ 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 { Upload, File as FileIcon, Loader2, ChevronDown, ChevronUp, X, FileText } from "lucide-react" import { getApiUrl } from "@/config/backend" +import { MultiDocsReportViewer } from "@/components/multi-docs-report-viewer" interface JobStatus { job_id: string @@ -15,14 +16,16 @@ interface JobStatus { processed_files: number total_files: number error?: string | null + report?: any | 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", + extracting: "Extracting content (PyMuPDF + Qwen2.5-VL)", + building_graph: "Building knowledge graph (DoWhy + Neo4j)", + indexing_vectors: "Indexing in vector database (Qdrant)", + generating_report: "Generating onboarding report (Claude AI)", completed: "Completed", failed: "Failed", } @@ -41,6 +44,7 @@ export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlinePro const [jobId, setJobId] = useState(null) const [status, setStatus] = useState(null) const [error, setError] = useState(null) + const [showReport, setShowReport] = useState(false) const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), []) @@ -148,12 +152,13 @@ export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlinePro 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% + stageProgress = 10 + Math.min(fileProgress * 0.3, 30) // 10-40% } else if (status.stage === "building_graph") { - stageProgress = 85 + Math.min(fileProgress * 0.5, 10) // 85-95% + stageProgress = 40 + Math.min(fileProgress * 0.2, 20) // 40-60% + } else if (status.stage === "indexing_vectors") { + stageProgress = 60 + Math.min(fileProgress * 0.15, 15) // 60-75% + } else if (status.stage === "generating_report") { + stageProgress = 75 + Math.min(fileProgress * 0.2, 20) // 75-95% } else if (status.stage === "completed") { stageProgress = 100 } else if (status.stage === "failed") { @@ -215,15 +220,36 @@ export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlinePro ✅ Knowledge graph created successfully! {status.total_files} file(s) processed. + {status.error && ( +
    + ⚠️ Note: {status.error} +
    + )}
    - +
    + + +
    + {showReport && jobId && ( +
    + setShowReport(false)} + /> +
    + )} ) : status?.stage === "failed" ? (
    diff --git a/src/components/multi-docs-upload-step.tsx b/src/components/multi-docs-upload-step.tsx index 8926f05..13d4798 100644 --- a/src/components/multi-docs-upload-step.tsx +++ b/src/components/multi-docs-upload-step.tsx @@ -5,8 +5,9 @@ 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 { Upload, File as FileIcon, Loader2, ArrowLeft, ArrowRight, FileText } from "lucide-react" import { getApiUrl } from "@/config/backend" +import { MultiDocsReportViewer } from "@/components/multi-docs-report-viewer" interface MultiDocsUploadStepProps { onBack: () => void @@ -25,9 +26,10 @@ interface JobStatus { 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", + extracting: "Extracting content (PyMuPDF + Qwen2.5-VL)", + building_graph: "Building knowledge graph (DoWhy + Neo4j)", + indexing_vectors: "Indexing in vector database (Qdrant)", + generating_report: "Generating onboarding report (Claude AI)", completed: "Completed", failed: "Failed", } @@ -38,6 +40,7 @@ export function MultiDocsUploadStep({ onBack, onNext }: MultiDocsUploadStepProps const [jobId, setJobId] = useState(null) const [status, setStatus] = useState(null) const [error, setError] = useState(null) + const [showReport, setShowReport] = useState(false) const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), []) @@ -214,6 +217,31 @@ export function MultiDocsUploadStep({ onBack, onNext }: MultiDocsUploadStepProps
    )} + {status?.stage === "completed" && jobId && !showReport && ( +
    + + + ✅ Processing completed! Report is ready. + + + +
    + )} + + {showReport && jobId && ( +
    + setShowReport(false)} + /> +
    + )} +