feat: Implement analysis job tracking with progress timeline and enhanced data source status management.

This commit is contained in:
2026-02-03 22:43:27 +00:00
parent c47614bc66
commit 358f2a42dd
22 changed files with 2251 additions and 219 deletions

View File

@@ -11,8 +11,9 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { ArrowRight, Globe, Loader2, Sparkles, AlertCircle, ArrowLeft } from 'lucide-react'
import type { EnhancedProductAnalysis, Keyword } from '@/lib/types'
import { useMutation } from 'convex/react'
import { useMutation, useQuery } from 'convex/react'
import { api } from '@/convex/_generated/api'
import { AnalysisTimeline } from '@/components/analysis-timeline'
const examples = [
{ name: 'Notion', url: 'https://notion.so' },
@@ -26,6 +27,7 @@ export default function OnboardingPage() {
const addDataSource = useMutation(api.dataSources.addDataSource)
const updateDataSourceStatus = useMutation(api.dataSources.updateDataSourceStatus)
const createAnalysis = useMutation(api.analyses.createAnalysis)
const createAnalysisJob = useMutation(api.analysisJobs.create)
const [url, setUrl] = useState('')
const [loading, setLoading] = useState(false)
const [progress, setProgress] = useState('')
@@ -36,33 +38,56 @@ export default function OnboardingPage() {
const [manualProductName, setManualProductName] = useState('')
const [manualDescription, setManualDescription] = useState('')
const [manualFeatures, setManualFeatures] = useState('')
const [pendingSourceId, setPendingSourceId] = useState<string | null>(null)
const [pendingProjectId, setPendingProjectId] = useState<string | null>(null)
const [pendingJobId, setPendingJobId] = useState<string | null>(null)
const analysisJob = useQuery(
api.analysisJobs.getById,
pendingJobId ? { jobId: pendingJobId as any } : "skip"
)
const persistAnalysis = async ({
analysis,
sourceUrl,
sourceName,
projectId,
dataSourceId,
}: {
analysis: EnhancedProductAnalysis
sourceUrl: string
sourceName: string
projectId?: string
dataSourceId?: string
}) => {
const { sourceId, projectId } = await addDataSource({
url: sourceUrl,
name: sourceName,
type: 'website',
})
const resolved = projectId && dataSourceId
? { projectId, sourceId: dataSourceId }
: await addDataSource({
url: sourceUrl,
name: sourceName,
type: 'website',
})
await createAnalysis({
projectId,
dataSourceId: sourceId,
analysis,
})
try {
await createAnalysis({
projectId: resolved.projectId,
dataSourceId: resolved.sourceId,
analysis,
})
await updateDataSourceStatus({
dataSourceId: sourceId,
analysisStatus: 'completed',
lastAnalyzedAt: Date.now(),
})
await updateDataSourceStatus({
dataSourceId: resolved.sourceId,
analysisStatus: 'completed',
lastAnalyzedAt: Date.now(),
})
} catch (err: any) {
await updateDataSourceStatus({
dataSourceId: resolved.sourceId,
analysisStatus: 'failed',
lastError: err?.message || 'Failed to save analysis',
lastAnalyzedAt: Date.now(),
})
throw err
}
}
async function analyzeWebsite() {
@@ -71,12 +96,35 @@ export default function OnboardingPage() {
setLoading(true)
setError('')
setProgress('Scraping website...')
let manualFallback = false
try {
const { sourceId, projectId } = await addDataSource({
url,
name: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
type: 'website',
})
await updateDataSourceStatus({
dataSourceId: sourceId,
analysisStatus: 'pending',
lastError: undefined,
lastAnalyzedAt: undefined,
})
setPendingSourceId(sourceId)
setPendingProjectId(projectId)
const jobId = await createAnalysisJob({
projectId,
dataSourceId: sourceId,
})
setPendingJobId(jobId)
const response = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
body: JSON.stringify({ url, jobId }),
})
if (response.redirected) {
@@ -90,6 +138,7 @@ export default function OnboardingPage() {
if (data.needsManualInput) {
setShowManualInput(true)
setManualProductName(url.replace(/^https?:\/\//, '').replace(/\/$/, ''))
manualFallback = true
throw new Error(data.error)
}
throw new Error(data.error || 'Failed to analyze')
@@ -106,8 +155,14 @@ export default function OnboardingPage() {
analysis: data.data,
sourceUrl: url,
sourceName: data.data.productName,
projectId,
dataSourceId: sourceId,
})
setPendingSourceId(null)
setPendingProjectId(null)
setPendingJobId(null)
setProgress('Redirecting to dashboard...')
// Redirect to dashboard with product name in query
@@ -116,6 +171,14 @@ export default function OnboardingPage() {
} catch (err: any) {
console.error('Analysis error:', err)
setError(err.message || 'Failed to analyze website')
if (pendingSourceId && !manualFallback) {
await updateDataSourceStatus({
dataSourceId: pendingSourceId,
analysisStatus: 'failed',
lastError: err?.message || 'Failed to analyze',
lastAnalyzedAt: Date.now(),
})
}
} finally {
setLoading(false)
}
@@ -172,13 +235,38 @@ export default function OnboardingPage() {
}
// Send to API to enhance with AI
let resolvedProjectId = pendingProjectId
let resolvedSourceId = pendingSourceId
let resolvedJobId = pendingJobId
if (!resolvedProjectId || !resolvedSourceId) {
const created = await addDataSource({
url: `manual:${manualProductName}`,
name: manualProductName,
type: 'website',
})
resolvedProjectId = created.projectId
resolvedSourceId = created.sourceId
setPendingProjectId(created.projectId)
setPendingSourceId(created.sourceId)
}
if (!resolvedJobId && resolvedProjectId && resolvedSourceId) {
resolvedJobId = await createAnalysisJob({
projectId: resolvedProjectId,
dataSourceId: resolvedSourceId,
})
setPendingJobId(resolvedJobId)
}
const response = await fetch('/api/analyze-manual', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productName: manualProductName,
description: manualDescription,
features: manualFeatures
features: manualFeatures,
jobId: resolvedJobId || undefined,
}),
})
@@ -206,18 +294,36 @@ export default function OnboardingPage() {
}))
setProgress('Saving analysis...')
const manualSourceUrl = pendingSourceId
? 'manual-input'
: `manual:${finalAnalysis.productName}`
await persistAnalysis({
analysis: finalAnalysis,
sourceUrl: 'manual-input',
sourceUrl: manualSourceUrl,
sourceName: finalAnalysis.productName,
projectId: resolvedProjectId || undefined,
dataSourceId: resolvedSourceId || undefined,
})
setPendingSourceId(null)
setPendingProjectId(null)
setPendingJobId(null)
// Redirect to dashboard
const params = new URLSearchParams({ product: finalAnalysis.productName })
router.push(`/dashboard?${params.toString()}`)
} catch (err: any) {
console.error('Manual analysis error:', err)
setError(err.message || 'Failed to analyze')
if (pendingSourceId) {
await updateDataSourceStatus({
dataSourceId: pendingSourceId,
analysisStatus: 'failed',
lastError: err?.message || 'Failed to analyze',
lastAnalyzedAt: Date.now(),
})
}
} finally {
setLoading(false)
}
@@ -294,10 +400,14 @@ export default function OnboardingPage() {
<Loader2 className="h-4 w-4 animate-spin" />
{progress}
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
</div>
{analysisJob?.timeline?.length ? (
<AnalysisTimeline items={analysisJob.timeline} />
) : (
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
</div>
)}
</div>
)}
@@ -400,11 +510,15 @@ export default function OnboardingPage() {
<Loader2 className="h-4 w-4 animate-spin" />
{progress}
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-3/5" />
</div>
{analysisJob?.timeline?.length ? (
<AnalysisTimeline items={analysisJob.timeline} />
) : (
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-3/5" />
</div>
)}
</div>
)}