qdrant db in multi doc
This commit is contained in:
parent
8a7f6f74dc
commit
5e89f25f54
52
src/app/api/multi-docs/jobs/[jobId]/report/pdf/route.ts
Normal file
52
src/app/api/multi-docs/jobs/[jobId]/report/pdf/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
src/app/api/multi-docs/jobs/[jobId]/report/route.ts
Normal file
48
src/app/api/multi-docs/jobs/[jobId]/report/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -94,14 +94,27 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
pollCount++;
|
pollCount++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Poll for repository sync status
|
// Poll for repository sync status from all_repositories table
|
||||||
const response = await authApiClient.get(`/api/vcs/${provider}/repositories?t=${Date.now()}`, {
|
// Use the endpoint that queries the database, not the GitHub API
|
||||||
headers: { 'x-user-id': user?.id }
|
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) {
|
if (response.data?.success) {
|
||||||
const repositories = response.data.data || [];
|
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) {
|
if (repo) {
|
||||||
const status = repo.sync_status;
|
const status = repo.sync_status;
|
||||||
@ -741,11 +754,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
console.log('🔍 Full error object:', err)
|
console.log('🔍 Full error object:', err)
|
||||||
console.log('🔍 Error response object:', err?.response)
|
console.log('🔍 Error response object:', err?.response)
|
||||||
|
|
||||||
// If backend signals auth required, show authentication button instead of auto-redirect
|
// If backend signals auth required, handle OAuth redirect
|
||||||
if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) {
|
// Check for requires_auth in response (status can be 200, 401, or any status)
|
||||||
console.log('🔐 Private repository detected, showing authentication button:', { status, requires_auth: data?.requires_auth, message: data?.message })
|
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 {
|
try {
|
||||||
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
||||||
repository_url: gitUrl.trim(),
|
repository_url: gitUrl.trim(),
|
||||||
@ -758,8 +772,14 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
console.warn('⚠️ Failed to store pending git attach:', e)
|
console.warn('⚠️ Failed to store pending git attach:', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't auto-redirect, let the user click the authenticate button
|
// Auto-redirect to OAuth
|
||||||
console.log('🔐 Private repository detected - user must click authenticate button')
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
238
src/components/multi-docs-report-viewer.tsx
Normal file
238
src/components/multi-docs-report-viewer.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -5,8 +5,9 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
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 { getApiUrl } from "@/config/backend"
|
||||||
|
import { MultiDocsReportViewer } from "@/components/multi-docs-report-viewer"
|
||||||
|
|
||||||
interface JobStatus {
|
interface JobStatus {
|
||||||
job_id: string
|
job_id: string
|
||||||
@ -15,14 +16,16 @@ interface JobStatus {
|
|||||||
processed_files: number
|
processed_files: number
|
||||||
total_files: number
|
total_files: number
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
report?: any | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const STAGE_COPY: Record<string, string> = {
|
const STAGE_COPY: Record<string, string> = {
|
||||||
received: "Job received",
|
received: "Job received",
|
||||||
saving_files: "Saving files",
|
saving_files: "Saving files",
|
||||||
extracting: "Extracting document content",
|
extracting: "Extracting content (PyMuPDF + Qwen2.5-VL)",
|
||||||
analyzing: "Calling Claude for causal relations",
|
building_graph: "Building knowledge graph (DoWhy + Neo4j)",
|
||||||
building_graph: "Writing to Neo4j knowledge graph",
|
indexing_vectors: "Indexing in vector database (Qdrant)",
|
||||||
|
generating_report: "Generating onboarding report (Claude AI)",
|
||||||
completed: "Completed",
|
completed: "Completed",
|
||||||
failed: "Failed",
|
failed: "Failed",
|
||||||
}
|
}
|
||||||
@ -41,6 +44,7 @@ export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlinePro
|
|||||||
const [jobId, setJobId] = useState<string | null>(null)
|
const [jobId, setJobId] = useState<string | null>(null)
|
||||||
const [status, setStatus] = useState<JobStatus | null>(null)
|
const [status, setStatus] = useState<JobStatus | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [showReport, setShowReport] = useState(false)
|
||||||
|
|
||||||
const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), [])
|
const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), [])
|
||||||
|
|
||||||
@ -148,12 +152,13 @@ export function MultiDocsUploadInline({ onJobCreated }: MultiDocsUploadInlinePro
|
|||||||
if (status.stage === "saving_files") {
|
if (status.stage === "saving_files") {
|
||||||
stageProgress = 5
|
stageProgress = 5
|
||||||
} else if (status.stage === "extracting") {
|
} else if (status.stage === "extracting") {
|
||||||
stageProgress = 10 + fileProgress // 10-40%
|
stageProgress = 10 + Math.min(fileProgress * 0.3, 30) // 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") {
|
} 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") {
|
} else if (status.stage === "completed") {
|
||||||
stageProgress = 100
|
stageProgress = 100
|
||||||
} else if (status.stage === "failed") {
|
} 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">
|
<Alert className="bg-green-500/10 border border-green-500/40 text-green-200">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
✅ Knowledge graph created successfully! {status.total_files} file(s) processed.
|
✅ 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>
|
</AlertDescription>
|
||||||
</Alert>
|
</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
|
<Button
|
||||||
onClick={resetUpload}
|
onClick={resetUpload}
|
||||||
variant="outline"
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{showReport && jobId && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<MultiDocsReportViewer
|
||||||
|
jobId={jobId}
|
||||||
|
onClose={() => setShowReport(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : status?.stage === "failed" ? (
|
) : status?.stage === "failed" ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Alert variant="destructive" className="bg-red-500/10 border border-red-500/40 text-red-200">
|
<Alert variant="destructive" className="bg-red-500/10 border border-red-500/40 text-red-200">
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
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 { getApiUrl } from "@/config/backend"
|
||||||
|
import { MultiDocsReportViewer } from "@/components/multi-docs-report-viewer"
|
||||||
|
|
||||||
interface MultiDocsUploadStepProps {
|
interface MultiDocsUploadStepProps {
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
@ -25,9 +26,10 @@ interface JobStatus {
|
|||||||
const STAGE_COPY: Record<string, string> = {
|
const STAGE_COPY: Record<string, string> = {
|
||||||
received: "Job received",
|
received: "Job received",
|
||||||
saving_files: "Saving files",
|
saving_files: "Saving files",
|
||||||
extracting: "Extracting document content",
|
extracting: "Extracting content (PyMuPDF + Qwen2.5-VL)",
|
||||||
analyzing: "Calling Claude for causal relations",
|
building_graph: "Building knowledge graph (DoWhy + Neo4j)",
|
||||||
building_graph: "Writing to Neo4j knowledge graph",
|
indexing_vectors: "Indexing in vector database (Qdrant)",
|
||||||
|
generating_report: "Generating onboarding report (Claude AI)",
|
||||||
completed: "Completed",
|
completed: "Completed",
|
||||||
failed: "Failed",
|
failed: "Failed",
|
||||||
}
|
}
|
||||||
@ -38,6 +40,7 @@ export function MultiDocsUploadStep({ onBack, onNext }: MultiDocsUploadStepProps
|
|||||||
const [jobId, setJobId] = useState<string | null>(null)
|
const [jobId, setJobId] = useState<string | null>(null)
|
||||||
const [status, setStatus] = useState<JobStatus | null>(null)
|
const [status, setStatus] = useState<JobStatus | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [showReport, setShowReport] = useState(false)
|
||||||
|
|
||||||
const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), [])
|
const uploadUrl = useMemo(() => getApiUrl("/api/multi-docs/jobs"), [])
|
||||||
|
|
||||||
@ -214,6 +217,31 @@ export function MultiDocsUploadStep({ onBack, onNext }: MultiDocsUploadStepProps
|
|||||||
</div>
|
</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">
|
<div className="flex justify-between pt-6">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@ -78,10 +78,14 @@ export async function attachRepository(payload: AttachRepositoryPayload, retries
|
|||||||
user_id: userId
|
user_id: userId
|
||||||
}, {
|
}, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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
|
// Normalize response
|
||||||
let parsed: any = response.data;
|
let parsed: any = response.data;
|
||||||
@ -98,18 +102,24 @@ export async function attachRepository(payload: AttachRepositoryPayload, retries
|
|||||||
success: (parsed?.success === true || parsed?.success === 'true')
|
success: (parsed?.success === true || parsed?.success === 'true')
|
||||||
};
|
};
|
||||||
|
|
||||||
// If authentication is required, return the response instead of throwing error
|
// If authentication is required, return the response immediately (even if success: false)
|
||||||
if (normalized.requires_auth || (normalized.message && normalized.message.includes('authentication'))) {
|
if (normalized.requires_auth || (normalized.message && (normalized.message.includes('authentication') || normalized.message.includes('authentication required')))) {
|
||||||
console.log('🔐 [attachRepository] Authentication required, returning response for UI to show authenticate button:', {
|
console.log('🔐 [attachRepository] Authentication required, returning response for OAuth redirect:', {
|
||||||
requires_auth: normalized.requires_auth,
|
requires_auth: normalized.requires_auth,
|
||||||
message: normalized.message,
|
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;
|
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;
|
return normalized;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// If it's the last retry or not a connection error, throw immediately
|
// If it's the last retry or not a connection error, throw immediately
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user