Files
SanatiLeads/app/(app)/dashboard/page.tsx
2026-02-04 01:05:00 +00:00

320 lines
15 KiB
TypeScript

"use client"
import { useQuery } from "convex/react"
import { useState } from "react"
import { api } from "@/convex/_generated/api"
import { useProject } from "@/components/project-context"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { useMutation } from "convex/react"
import { Progress } from "@/components/ui/progress"
export default function Page() {
const { selectedProjectId } = useProject()
const projects = useQuery(api.projects.getProjects)
const dataSources = useQuery(
api.dataSources.getProjectDataSources,
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
)
const searchContext = useQuery(
api.projects.getSearchContext,
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
)
const analysisJobs = useQuery(
api.analysisJobs.listByProject,
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
)
const updateDataSourceStatus = useMutation(api.dataSources.updateDataSourceStatus)
const createAnalysis = useMutation(api.analyses.createAnalysis)
const createAnalysisJob = useMutation(api.analysisJobs.create)
const [reanalyzingId, setReanalyzingId] = useState<string | null>(null)
const analysis = useQuery(
api.analyses.getLatestByProject,
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
)
const selectedProject = projects?.find((project) => project._id === selectedProjectId)
const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || []
const isLoading = selectedProjectId && analysis === undefined
if (!selectedProjectId && projects && projects.length === 0) {
return (
<div className="flex flex-1 items-center justify-center p-10 text-center">
<div className="space-y-2">
<h2 className="text-xl font-semibold">No projects yet</h2>
<p className="text-muted-foreground">Complete onboarding to create your first project.</p>
</div>
</div>
)
}
if (isLoading) {
return (
<div className="flex flex-1 items-center justify-center p-10 text-center text-muted-foreground">
Loading analysis...
</div>
)
}
if (!analysis) {
return (
<div className="flex flex-1 items-center justify-center p-10 text-center">
<div className="space-y-2">
<h2 className="text-xl font-semibold">No analysis yet</h2>
<p className="text-muted-foreground">Run onboarding to analyze a product for this project.</p>
</div>
</div>
)
}
const handleReanalyze = async (source: any) => {
if (!selectedProjectId) return
setReanalyzingId(source._id)
await updateDataSourceStatus({
dataSourceId: source._id,
analysisStatus: "pending",
lastError: undefined,
lastAnalyzedAt: undefined,
})
try {
const jobId = await createAnalysisJob({
projectId: selectedProjectId as any,
dataSourceId: source._id,
})
const response = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: source.url, jobId }),
})
const data = await response.json()
if (!response.ok) {
await updateDataSourceStatus({
dataSourceId: source._id,
analysisStatus: "failed",
lastError: data.error || "Analysis failed",
lastAnalyzedAt: Date.now(),
})
return
}
await createAnalysis({
projectId: selectedProjectId as any,
dataSourceId: source._id,
analysis: data.data,
})
await updateDataSourceStatus({
dataSourceId: source._id,
analysisStatus: "completed",
lastError: undefined,
lastAnalyzedAt: Date.now(),
})
} finally {
setReanalyzingId(null)
}
}
return (
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-8">
<div className="space-y-2">
<div className="flex items-center gap-2">
<h1 className="text-2xl font-semibold">
{selectedProject?.name || analysis.productName}
</h1>
<Badge variant="outline">{analysis.productName}</Badge>
</div>
<p className="text-muted-foreground">{analysis.tagline}</p>
<p className="max-w-3xl text-sm text-muted-foreground">{analysis.description}</p>
</div>
{searchContext?.missingSources?.length > 0 && (
<Alert>
<AlertDescription>
Some selected sources don&apos;t have analysis yet:{" "}
{searchContext.missingSources
.map((missing: any) =>
dataSources?.find((source: any) => source._id === missing.sourceId)?.name ||
dataSources?.find((source: any) => source._id === missing.sourceId)?.url ||
missing.sourceId
)
.join(", ")}
. Run onboarding or re-analyze them for best results.
</AlertDescription>
</Alert>
)}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Key Features</CardTitle>
</CardHeader>
<CardContent className="text-2xl font-semibold">{analysis.features.length}</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Search Keywords</CardTitle>
</CardHeader>
<CardContent className="text-2xl font-semibold">{analysis.keywords.length}</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Target Users</CardTitle>
</CardHeader>
<CardContent className="text-2xl font-semibold">{analysis.personas.length}</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Competitors</CardTitle>
</CardHeader>
<CardContent className="text-2xl font-semibold">{analysis.competitors.length}</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Use Cases</CardTitle>
</CardHeader>
<CardContent className="text-2xl font-semibold">{analysis.useCases.length}</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">Sources</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
{dataSources && dataSources.length > 0 ? (
dataSources.map((source: any) => (
<div key={source._id} className="flex items-center justify-between gap-3">
<span className="truncate">{source.name || source.url}</span>
<div className="flex items-center gap-2">
<Badge variant={selectedSourceIds.includes(source._id) ? "secondary" : "outline"}>
{selectedSourceIds.includes(source._id) ? "active" : "inactive"}
</Badge>
<Badge variant={source.analysisStatus === "completed" ? "secondary" : "outline"}>
{source.analysisStatus}
</Badge>
<Button
size="sm"
variant="outline"
onClick={() => handleReanalyze(source)}
disabled={reanalyzingId === source._id}
>
{reanalyzingId === source._id ? "Analyzing..." : "Re-analyze"}
</Button>
</div>
</div>
))
) : (
<p>No data sources yet. Add a source during onboarding.</p>
)}
</CardContent>
</Card>
{analysisJobs && analysisJobs.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base">Runs</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
{analysisJobs.slice(0, 5).map((job: any) => {
const sourceName = dataSources?.find((source: any) => source._id === job.dataSourceId)?.name
|| dataSources?.find((source: any) => source._id === job.dataSourceId)?.url
|| "Unknown source"
return (
<div key={job._id} className="space-y-2 rounded-md border border-border p-3">
<div className="flex items-center justify-between gap-3">
<div className="truncate">
<span className="font-medium text-foreground">{sourceName}</span>{" "}
<span className="text-muted-foreground">
({job.status})
</span>
</div>
{job.status === "failed" && (
<Button
size="sm"
variant="outline"
onClick={() => {
const source = dataSources?.find((s: any) => s._id === job.dataSourceId)
if (source) void handleReanalyze(source)
}}
>
Retry
</Button>
)}
</div>
{(job.status === "running" || job.status === "pending") && (
<div className="space-y-1">
<Progress value={typeof job.progress === "number" ? job.progress : 10} />
<div className="text-xs text-muted-foreground">
{typeof job.progress === "number" ? `${job.progress}% complete` : "Starting..."}
</div>
</div>
)}
{job.status === "failed" && job.error && (
<div className="text-xs text-destructive">{job.error}</div>
)}
</div>
)
})}
</CardContent>
</Card>
)}
{searchContext?.context && (
<Card>
<CardHeader>
<CardTitle className="text-base">Combined Summary</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 text-sm text-muted-foreground md:grid-cols-2">
<div>Keywords: <span className="text-foreground font-medium">{searchContext.context.keywords.length}</span></div>
<div>Problems: <span className="text-foreground font-medium">{searchContext.context.problemsSolved.length}</span></div>
<div>Competitors: <span className="text-foreground font-medium">{searchContext.context.competitors.length}</span></div>
<div>Use Cases: <span className="text-foreground font-medium">{searchContext.context.useCases.length}</span></div>
</CardContent>
</Card>
)}
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base">Key Features</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{analysis.features.slice(0, 6).map((feature, index) => (
<div key={`${feature.name}-${index}`} className="flex items-start gap-2">
<span className="mt-1 h-2 w-2 rounded-full bg-primary" />
<div>
<p className="text-sm font-medium">{feature.name}</p>
<p className="text-xs text-muted-foreground">{feature.description}</p>
</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Top Problems</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{analysis.problemsSolved.slice(0, 6).map((problem, index) => (
<div key={`${problem.problem}-${index}`} className="flex items-start gap-2">
<span className="mt-1 h-2 w-2 rounded-full bg-primary" />
<div>
<p className="text-sm font-medium">{problem.problem}</p>
<p className="text-xs text-muted-foreground">{problem.emotionalImpact}</p>
</div>
</div>
))}
</CardContent>
</Card>
</div>
</div>
)
}