"use client" import * as React from "react" import Link from "next/link" import { usePathname } from "next/navigation" import { Command, Frame, HelpCircle, Settings, Settings2, Terminal, Target, Plus, ArrowUpRight, ChevronsUpDown } from "lucide-react" import { NavUser } from "@/components/nav-user" import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, } from "@/components/ui/sidebar" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { useQuery, useMutation } from "convex/react" import { api } from "@/convex/_generated/api" import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label" import { useProject } from "@/components/project-context" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { AnalysisTimeline } from "@/components/analysis-timeline" export function AppSidebar({ ...props }: React.ComponentProps) { const pathname = usePathname() const projects = useQuery(api.projects.getProjects); const currentUser = useQuery(api.users.getCurrent); const { selectedProjectId, setSelectedProjectId } = useProject(); 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 updateAnalysisJob = useMutation(api.analysisJobs.update); const [isAdding, setIsAdding] = React.useState(false); const [isCreatingProject, setIsCreatingProject] = React.useState(false); const [projectName, setProjectName] = React.useState(""); const [projectDefault, setProjectDefault] = React.useState(true); const [projectError, setProjectError] = React.useState(null); const [isSubmittingProject, setIsSubmittingProject] = React.useState(false); const createProject = useMutation(api.projects.createProject); const updateProject = useMutation(api.projects.updateProject); const [isEditingProject, setIsEditingProject] = React.useState(false); const [editingProjectId, setEditingProjectId] = React.useState(null); const [editingProjectName, setEditingProjectName] = React.useState(""); const [editingProjectDefault, setEditingProjectDefault] = React.useState(false); const [editingProjectError, setEditingProjectError] = React.useState(null); const [isSubmittingEdit, setIsSubmittingEdit] = React.useState(false); const [sourceUrl, setSourceUrl] = React.useState(""); const [sourceName, setSourceName] = React.useState(""); const [sourceError, setSourceError] = React.useState(null); const [sourceNotice, setSourceNotice] = React.useState(null); const [isSubmittingSource, setIsSubmittingSource] = React.useState(false); const [manualMode, setManualMode] = React.useState(false); const [manualProductName, setManualProductName] = React.useState(""); const [manualDescription, setManualDescription] = React.useState(""); const [manualFeatures, setManualFeatures] = React.useState(""); const [pendingSourceId, setPendingSourceId] = React.useState(null); const [pendingProjectId, setPendingProjectId] = React.useState(null); const [pendingJobId, setPendingJobId] = React.useState(null); const analysisJob = useQuery( api.analysisJobs.getById, pendingJobId ? { jobId: pendingJobId as any } : "skip" ); // Set default selected project React.useEffect(() => { if (projects && projects.length > 0 && !selectedProjectId) { // Prefer default project, otherwise first const defaultProj = projects.find(p => p.isDefault); setSelectedProjectId(defaultProj ? defaultProj._id : projects[0]._id); } }, [projects, selectedProjectId]); // Data Sources Query const dataSources = useQuery( api.dataSources.getProjectDataSources, selectedProjectId ? { projectId: selectedProjectId as any } : "skip" ); const toggleConfig = useMutation(api.projects.toggleDataSourceConfig); const selectedProject = projects?.find(p => p._id === selectedProjectId); const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || []; const selectedProjectName = selectedProject?.name || "Select Project"; const handleToggle = async (sourceId: string, checked: boolean) => { if (!selectedProjectId) return; await toggleConfig({ projectId: selectedProjectId as any, sourceId: sourceId as any, selected: checked }); }; const handleAddSource = async () => { if (!sourceUrl) { setSourceError("Please enter a URL."); return; } setSourceError(null); setIsSubmittingSource(true); try { const result = await addDataSource({ projectId: selectedProjectId as any, url: sourceUrl, name: sourceName || sourceUrl, type: "website", }); if ((result as any).isExisting) { setSourceNotice("This source already exists and was reused."); } const jobId = await createAnalysisJob({ projectId: result.projectId, dataSourceId: result.sourceId, }); setPendingSourceId(result.sourceId); setPendingProjectId(result.projectId); setPendingJobId(jobId); await updateDataSourceStatus({ dataSourceId: result.sourceId, analysisStatus: "pending", lastError: undefined, lastAnalyzedAt: undefined, }); const response = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: sourceUrl, jobId }), }); const data = await response.json(); if (!response.ok) { if (data.needsManualInput) { setManualMode(true); setManualProductName( sourceName || sourceUrl.replace(/^https?:\/\//, "").replace(/\/$/, "") ); setPendingSourceId(result.sourceId); setPendingProjectId(result.projectId); setSourceError(data.error || "Manual input required."); return; } await updateDataSourceStatus({ dataSourceId: result.sourceId, analysisStatus: "failed", lastError: data.error || "Analysis failed", lastAnalyzedAt: Date.now(), }); throw new Error(data.error || "Analysis failed"); } await createAnalysis({ projectId: result.projectId, dataSourceId: result.sourceId, analysis: data.data, }); await updateDataSourceStatus({ dataSourceId: result.sourceId, analysisStatus: "completed", lastError: undefined, lastAnalyzedAt: Date.now(), }); setSourceUrl(""); setSourceName(""); setSourceNotice(null); setManualMode(false); setManualProductName(""); setManualDescription(""); setManualFeatures(""); setPendingSourceId(null); setPendingProjectId(null); setPendingJobId(null); setIsAdding(false); } catch (err: any) { setSourceError(err?.message || "Failed to add source."); } finally { setIsSubmittingSource(false); } }; const handleManualAnalyze = async () => { if (!manualProductName || !manualDescription) { setSourceError("Product name and description are required."); return; } if (!pendingSourceId || !pendingProjectId) { setSourceError("Missing pending source."); return; } setIsSubmittingSource(true); setSourceError(null); try { const response = await fetch("/api/analyze-manual", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ productName: manualProductName, description: manualDescription, features: manualFeatures, jobId: pendingJobId || undefined, }), }); const data = await response.json(); if (!response.ok) { await updateDataSourceStatus({ dataSourceId: pendingSourceId as any, analysisStatus: "failed", lastError: data.error || "Manual analysis failed", lastAnalyzedAt: Date.now(), }); throw new Error(data.error || "Manual analysis failed"); } await createAnalysis({ projectId: pendingProjectId as any, dataSourceId: pendingSourceId as any, analysis: data.data, }); await updateDataSourceStatus({ dataSourceId: pendingSourceId as any, analysisStatus: "completed", lastError: undefined, lastAnalyzedAt: Date.now(), }); setSourceUrl(""); setSourceName(""); setManualMode(false); setManualProductName(""); setManualDescription(""); setManualFeatures(""); setPendingSourceId(null); setPendingProjectId(null); setPendingJobId(null); setIsAdding(false); } catch (err: any) { setSourceError(err?.message || "Manual analysis failed."); } finally { setIsSubmittingSource(false); } }; return (
{selectedProjectName} Projects
Switch project {projects?.map((project) => ( { if (event.defaultPrevented) return setSelectedProjectId(project._id) }} >
{project.name}
))} setIsCreatingProject(true)}> Create Project
{/* Platform Nav */} Platform Dashboard Opportunities Settings Help {/* Data Sources Config */} {selectedProjectId && ( Active Data Sources ({selectedProject?.name})
{(!dataSources || dataSources.length === 0) && (
No data sources yet.
)} {dataSources?.map((source) => (
handleToggle(source._id, checked === true)} />
Details
))}
)}
{currentUser && (currentUser.name || currentUser.email) && ( )} { if (!open && manualMode && pendingSourceId) { updateDataSourceStatus({ dataSourceId: pendingSourceId as any, analysisStatus: "failed", lastError: "Manual input cancelled", lastAnalyzedAt: Date.now(), }); } if (!open && manualMode && pendingJobId) { updateAnalysisJob({ jobId: pendingJobId as any, status: "failed", error: "Manual input cancelled", }); } if (!open) { setManualMode(false); setSourceError(null); setSourceNotice(null); setPendingSourceId(null); setPendingProjectId(null); setPendingJobId(null); } setIsAdding(open); }} > Add Data Source Add a website to analyze for this project.
{!manualMode && ( <>
setSourceUrl(event.target.value)} disabled={isSubmittingSource} />
setSourceName(event.target.value)} disabled={isSubmittingSource} />
)} {manualMode && ( <>
setManualProductName(event.target.value)} disabled={isSubmittingSource} />