qdrant db in multi doc

This commit is contained in:
Pradeep 2025-12-01 09:07:10 +05:30
parent 8a7f6f74dc
commit 5e89f25f54
7 changed files with 459 additions and 37 deletions

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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
}

View File

@ -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<string, string>
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<ProjectReport | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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 (
<Card className="bg-white/5 border border-white/10">
<CardContent className="p-8 text-center">
<Loader2 className="h-8 w-8 animate-spin text-orange-400 mx-auto mb-4" />
<p className="text-white/60">Loading report...</p>
</CardContent>
</Card>
)
}
if (error) {
return (
<Card className="bg-white/5 border border-white/10">
<CardContent className="p-8">
<Alert variant="destructive" className="bg-red-500/10 border border-red-500/40 text-red-200">
<AlertDescription>{error}</AlertDescription>
</Alert>
{onClose && (
<Button
onClick={onClose}
variant="outline"
className="mt-4 w-full border-white/20 text-white hover:bg-white/10"
>
Close
</Button>
)}
</CardContent>
</Card>
)
}
if (!report) {
return (
<Card className="bg-white/5 border border-white/10">
<CardContent className="p-8 text-center">
<p className="text-white/60">No report available</p>
{onClose && (
<Button
onClick={onClose}
variant="outline"
className="mt-4 border-white/20 text-white hover:bg-white/10"
>
Close
</Button>
)}
</CardContent>
</Card>
)
}
return (
<Card className="bg-white/5 border border-white/10">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<FileText className="h-5 w-5 text-orange-400" />
<CardTitle className="text-lg text-white">{report.title}</CardTitle>
</div>
<div className="flex items-center space-x-2">
<Button
onClick={handleDownloadPDF}
disabled={downloading || !report.metadata?.pdf_path}
className="bg-orange-500 hover:bg-orange-600 text-black font-semibold"
size="sm"
>
{downloading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Downloading...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" /> Download PDF
</>
)}
</Button>
{onClose && (
<Button
onClick={onClose}
variant="ghost"
size="sm"
className="text-white/60 hover:text-white hover:bg-white/10"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
</div>
<div className="flex items-center space-x-4 mt-2 text-xs text-white/60">
<span>{report.total_pages} pages</span>
{report.metadata?.total_relations && (
<span>{report.metadata.total_relations} relationships</span>
)}
{report.metadata?.total_concepts && (
<span>{report.metadata.total_concepts} concepts</span>
)}
<span>
Generated: {new Date(report.generated_at).toLocaleDateString()}
</span>
</div>
</CardHeader>
<CardContent>
<div className="bg-black/20 rounded-lg p-6 border border-white/10 max-h-[600px] overflow-y-auto">
<div
className="text-white/90 markdown-content prose prose-invert"
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{report.content.split('\n').map((line, idx) => {
// Headers
if (line.match(/^### /)) {
return <h3 key={idx} className="text-lg font-semibold text-white mb-2 mt-4">{line.replace(/^### /, '')}</h3>
}
if (line.match(/^## /)) {
return <h2 key={idx} className="text-xl font-semibold text-orange-300 mb-3 mt-5 border-b border-white/10 pb-1">{line.replace(/^## /, '')}</h2>
}
if (line.match(/^# /)) {
return <h1 key={idx} className="text-2xl font-bold text-orange-400 mb-4 mt-6 border-b border-orange-400/40 pb-2">{line.replace(/^# /, '')}</h1>
}
// Lists
if (line.match(/^[\-\*] /)) {
return <li key={idx} className="text-white/80 ml-4 list-disc">{line.replace(/^[\-\*] /, '')}</li>
}
if (line.match(/^\d+\. /)) {
return <li key={idx} className="text-white/80 ml-4 list-decimal">{line.replace(/^\d+\. /, '')}</li>
}
// Code blocks
if (line.match(/^```/)) {
return <br key={idx} />
}
// Regular paragraphs
if (line.trim() === '') {
return <br key={idx} />
}
return <p key={idx} className="text-white/80 mb-3 leading-relaxed">{line}</p>
})}
</div>
</div>
</CardContent>
</Card>
)
}

View File

@ -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<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",
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<string | null>(null)
const [status, setStatus] = useState<JobStatus | null>(null)
const [error, setError] = useState<string | null>(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,16 +220,37 @@ export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlinePro
<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.
{status.error && (
<div className="mt-2 text-xs text-yellow-300">
Note: {status.error}
</div>
)}
</AlertDescription>
</Alert>
<div className="flex gap-2">
<Button
onClick={() => setShowReport(true)}
className="flex-1 bg-orange-500 hover:bg-orange-600 text-black font-semibold"
>
<FileText className="mr-2 h-4 w-4" /> View Report
</Button>
<Button
onClick={resetUpload}
variant="outline"
className="w-full border-white/20 text-white hover:bg-white/10"
className="flex-1 border-white/20 text-white hover:bg-white/10"
>
Upload More Documents
Upload More
</Button>
</div>
{showReport && jobId && (
<div className="mt-4">
<MultiDocsReportViewer
jobId={jobId}
onClose={() => setShowReport(false)}
/>
</div>
)}
</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">

View File

@ -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<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",
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<string | null>(null)
const [status, setStatus] = useState<JobStatus | null>(null)
const [error, setError] = useState<string | null>(null)
const [showReport, setShowReport] = useState(false)
const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), [])
@ -214,6 +217,31 @@ export function MultiDocsUploadStep({ onBack, onNext }: MultiDocsUploadStepProps
</div>
)}
{status?.stage === "completed" && jobId && !showReport && (
<div className="space-y-3 pt-4 border-t border-white/10">
<Alert className="bg-green-500/10 border border-green-500/40 text-green-200">
<AlertDescription>
Processing completed! Report is ready.
</AlertDescription>
</Alert>
<Button
onClick={() => setShowReport(true)}
className="w-full bg-orange-500 hover:bg-orange-600 text-black font-semibold"
>
<FileText className="mr-2 h-4 w-4" /> View Onboarding Report
</Button>
</div>
)}
{showReport && jobId && (
<div className="pt-4 border-t border-white/10">
<MultiDocsReportViewer
jobId={jobId}
onClose={() => setShowReport(false)}
/>
</div>
)}
<div className="flex justify-between pt-6">
<Button
variant="outline"

View File

@ -78,10 +78,14 @@ export async function attachRepository(payload: AttachRepositoryPayload, retries
user_id: userId
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 60000 // 60 seconds for repository operations
timeout: 60000, // 60 seconds for repository operations
validateStatus: (status) => {
// Accept 200, 201, and also 401/403 if they contain requires_auth
return status >= 200 && status < 300 || status === 401 || status === 403;
}
});
console.log(`📡 [attachRepository] ${provider.toUpperCase()} response:`, response.data);
console.log(`📡 [attachRepository] ${provider.toUpperCase()} response status: ${response.status}, data:`, response.data);
// Normalize response
let parsed: any = response.data;
@ -98,18 +102,24 @@ export async function attachRepository(payload: AttachRepositoryPayload, retries
success: (parsed?.success === true || parsed?.success === 'true')
};
// If authentication is required, return the response instead of throwing error
if (normalized.requires_auth || (normalized.message && normalized.message.includes('authentication'))) {
console.log('🔐 [attachRepository] Authentication required, returning response for UI to show authenticate button:', {
// If authentication is required, return the response immediately (even if success: false)
if (normalized.requires_auth || (normalized.message && (normalized.message.includes('authentication') || normalized.message.includes('authentication required')))) {
console.log('🔐 [attachRepository] Authentication required, returning response for OAuth redirect:', {
requires_auth: normalized.requires_auth,
message: normalized.message,
auth_url: normalized.auth_url
auth_url: normalized.auth_url,
status: response.status
});
// Return the response so UI can show authenticate button
// Return the response so UI can redirect to OAuth
return normalized;
}
// If success is false and no auth required, it's a real error
if (!normalized.success && !normalized.requires_auth) {
throw new Error(normalized.message || 'Failed to attach repository');
}
return normalized;
} catch (error: any) {
// If it's the last retry or not a connection error, throw immediately