Files
SanatiLeads/app/app/(app)/data-sources/[id]/page.tsx
2026-02-04 11:18:33 +00:00

422 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useParams, useRouter } from "next/navigation"
import { useMutation, useQuery } from "convex/react"
import { api } from "@/convex/_generated/api"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Settings } from "lucide-react"
import * as React from "react"
import { ProfileSectionEditor, SectionEditor } from "@/components/analysis-section-editor"
import { AnalysisTimeline } from "@/components/analysis-timeline"
function formatDate(timestamp?: number) {
if (!timestamp) return "Not analyzed yet";
return new Date(timestamp).toLocaleString();
}
export default function DataSourceDetailPage() {
const params = useParams<{ id: string }>()
const router = useRouter()
const dataSourceId = params?.id
const dataSource = useQuery(
api.dataSources.getById,
dataSourceId ? { dataSourceId: dataSourceId as any } : "skip"
)
const analysis = useQuery(
api.analyses.getLatestByDataSource,
dataSourceId ? { dataSourceId: dataSourceId as any } : "skip"
)
const analysisJob = useQuery(
api.analysisJobs.getLatestByDataSource,
dataSourceId ? { dataSourceId: dataSourceId as any } : "skip"
)
const sections = useQuery(
api.analysisSections.listByAnalysis,
analysis?._id ? { analysisId: analysis._id as any } : "skip"
)
const removeDataSource = useMutation(api.dataSources.remove)
const updateDataSourceStatus = useMutation(api.dataSources.updateDataSourceStatus)
const createAnalysisJob = useMutation(api.analysisJobs.create)
const [isDeleting, setIsDeleting] = React.useState(false)
const [isDialogOpen, setIsDialogOpen] = React.useState(false)
const [isReanalyzing, setIsReanalyzing] = React.useState(false)
const [reanalysisError, setReanalysisError] = React.useState<string | null>(null)
const sectionMap = React.useMemo(() => {
const map = new Map<string, any>()
sections?.forEach((section: any) => {
map.set(section.sectionKey, section.items)
})
return map
}, [sections])
if (dataSource === undefined) {
return (
<div className="p-8">
<div className="text-sm text-muted-foreground">Loading data source</div>
</div>
)
}
if (!dataSource) {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold">Data source not found</h1>
<p className="mt-2 text-sm text-muted-foreground">
This data source may have been removed or you no longer have access.
</p>
</div>
)
}
const handleReanalyze = async () => {
if (!dataSource || !dataSource.url) return
if (dataSource.url.startsWith("manual:")) {
setReanalysisError("Manual sources cant be reanalyzed automatically.");
return;
}
setIsReanalyzing(true)
setReanalysisError(null)
try {
await updateDataSourceStatus({
dataSourceId: dataSource._id as any,
analysisStatus: "pending",
lastError: undefined,
lastAnalyzedAt: undefined,
})
const jobId = await createAnalysisJob({
projectId: dataSource.projectId,
dataSourceId: dataSource._id,
})
await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: dataSource.url, jobId }),
})
} catch (err: any) {
setReanalysisError(err?.message || "Failed to reanalyze data source.");
await updateDataSourceStatus({
dataSourceId: dataSource._id as any,
analysisStatus: "failed",
lastError: err?.message || "Failed to reanalyze data source.",
lastAnalyzedAt: Date.now(),
})
} finally {
setIsReanalyzing(false)
}
}
const statusVariant =
dataSource.analysisStatus === "completed"
? "secondary"
: dataSource.analysisStatus === "failed"
? "destructive"
: "outline"
return (
<div className="flex flex-1 flex-col gap-6 p-8">
<div className="flex flex-col gap-2">
<div className="flex flex-wrap items-start justify-between gap-3">
<h1 className="text-3xl font-semibold">
{dataSource.name || "Data Source"}
</h1>
<div className="flex items-center gap-2">
<Badge variant={statusVariant}>
{dataSource.analysisStatus}
</Badge>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
aria-label="Data source settings"
>
<Settings className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={handleReanalyze} disabled={isReanalyzing}>
{isReanalyzing ? "Reanalyzing..." : "Run reanalysis"}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => setIsDialogOpen(true)}
className="text-destructive focus:text-destructive"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<p className="text-sm text-muted-foreground">{dataSource.url}</p>
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
<span>Last analyzed: {formatDate(dataSource.lastAnalyzedAt)}</span>
{reanalysisError && (
<span className="text-destructive">{reanalysisError}</span>
)}
{dataSource.lastError && (
<span className="text-destructive">
Error: {dataSource.lastError}
</span>
)}
</div>
</div>
{analysisJob?.timeline?.length && analysisJob.status !== "completed" ? (
<Card>
<CardHeader>
<CardTitle className="text-base">Analysis Progress</CardTitle>
</CardHeader>
<CardContent>
<AnalysisTimeline items={analysisJob.timeline} />
</CardContent>
</Card>
) : null}
{analysis ? (
<>
<div className="grid gap-4 md:grid-cols-3">
<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>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<div>
<span className="font-medium text-foreground">Product:</span>{" "}
{analysis.productName}
</div>
<div>
<span className="font-medium text-foreground">Tagline:</span>{" "}
{analysis.tagline}
</div>
<div>
<span className="font-medium text-foreground">Category:</span>{" "}
{analysis.category}
</div>
<div>
<span className="font-medium text-foreground">Positioning:</span>{" "}
{analysis.positioning}
</div>
<div className="pt-2">{analysis.description}</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-3 text-sm">
{analysis.features.slice(0, 6).map((feature) => (
<div key={feature.name}>
<div className="font-medium">{feature.name}</div>
<div className="text-muted-foreground">
{feature.description}
</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Problems</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
{analysis.problemsSolved.slice(0, 6).map((problem) => (
<div key={problem.problem}>
<div className="font-medium">{problem.problem}</div>
<div className="text-muted-foreground">
Severity: {problem.severity} · {problem.emotionalImpact}
</div>
</div>
))}
</CardContent>
</Card>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base">Target Users</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
{analysis.personas.slice(0, 4).map((persona) => (
<div key={`${persona.name}-${persona.role}`}>
<div className="font-medium">
{persona.name} · {persona.role}
</div>
<div className="text-muted-foreground">
{persona.industry} · {persona.companySize}
</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Search Keywords</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{analysis.keywords.slice(0, 12).map((keyword) => (
<Badge key={keyword.term} variant="outline">
{keyword.term}
</Badge>
))}
</CardContent>
</Card>
</div>
<div className="space-y-4">
<h2 className="text-lg font-semibold">Edit Sections</h2>
<div className="grid gap-4 lg:grid-cols-3">
<ProfileSectionEditor
analysisId={analysis._id as any}
items={
sectionMap.get("profile") || {
productName: analysis.productName,
tagline: analysis.tagline,
description: analysis.description,
category: analysis.category,
positioning: analysis.positioning,
}
}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="features"
title="Key Features"
items={sectionMap.get("features") || analysis.features}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="competitors"
title="Competitors"
items={sectionMap.get("competitors") || analysis.competitors}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="keywords"
title="Search Keywords"
items={sectionMap.get("keywords") || analysis.keywords}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="problems"
title="Problems"
items={sectionMap.get("problems") || analysis.problemsSolved}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="personas"
title="Target Users"
items={sectionMap.get("personas") || analysis.personas}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="useCases"
title="Use Cases"
items={sectionMap.get("useCases") || analysis.useCases}
/>
<SectionEditor
analysisId={analysis._id as any}
sectionKey="dorkQueries"
title="Search Queries"
items={sectionMap.get("dorkQueries") || analysis.dorkQueries}
/>
</div>
</div>
</>
) : (
<Card>
<CardHeader>
<CardTitle className="text-base">Full Analysis</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
No analysis available yet. Trigger a new analysis to populate this
data source.
</CardContent>
</Card>
)}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete data source</DialogTitle>
<DialogDescription>
This removes the data source and its analyses from the project. This
cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsDialogOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={async () => {
if (!dataSourceId) return
setIsDeleting(true)
await removeDataSource({ dataSourceId: dataSourceId as any })
router.push("/app/dashboard")
}}
disabled={isDeleting}
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}