337 lines
15 KiB
TypeScript
337 lines
15 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import Link from "next/link"
|
|
import { usePathname } from "next/navigation"
|
|
import {
|
|
Command,
|
|
Frame,
|
|
HelpCircle,
|
|
Settings2,
|
|
Terminal,
|
|
Target,
|
|
Plus
|
|
} 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 { 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"
|
|
|
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
const pathname = usePathname()
|
|
const projects = useQuery(api.projects.getProjects);
|
|
const { selectedProjectId, setSelectedProjectId } = useProject();
|
|
const addDataSource = useMutation(api.dataSources.addDataSource);
|
|
const updateDataSourceStatus = useMutation(api.dataSources.updateDataSourceStatus);
|
|
const createAnalysis = useMutation(api.analyses.createAnalysis);
|
|
const [isAdding, setIsAdding] = React.useState(false);
|
|
const [sourceUrl, setSourceUrl] = React.useState("");
|
|
const [sourceName, setSourceName] = React.useState("");
|
|
const [sourceError, setSourceError] = React.useState<string | null>(null);
|
|
const [isSubmittingSource, setIsSubmittingSource] = React.useState(false);
|
|
|
|
// 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 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 { sourceId, projectId } = await addDataSource({
|
|
projectId: selectedProjectId as any,
|
|
url: sourceUrl,
|
|
name: sourceName || sourceUrl,
|
|
type: "website",
|
|
});
|
|
|
|
await updateDataSourceStatus({
|
|
dataSourceId: 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 }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
await updateDataSourceStatus({
|
|
dataSourceId: sourceId,
|
|
analysisStatus: "failed",
|
|
lastError: data.error || "Analysis failed",
|
|
lastAnalyzedAt: Date.now(),
|
|
});
|
|
throw new Error(data.error || "Analysis failed");
|
|
}
|
|
|
|
await createAnalysis({
|
|
projectId,
|
|
dataSourceId: sourceId,
|
|
analysis: data.data,
|
|
});
|
|
|
|
await updateDataSourceStatus({
|
|
dataSourceId: sourceId,
|
|
analysisStatus: "completed",
|
|
lastError: undefined,
|
|
lastAnalyzedAt: Date.now(),
|
|
});
|
|
|
|
setSourceUrl("");
|
|
setSourceName("");
|
|
setIsAdding(false);
|
|
} catch (err: any) {
|
|
setSourceError(err?.message || "Failed to add source.");
|
|
} finally {
|
|
setIsSubmittingSource(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Sidebar variant="inset" {...props}>
|
|
<SidebarHeader>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton size="lg" asChild>
|
|
<a href="#">
|
|
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
<Command className="size-4" />
|
|
</div>
|
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
<span className="truncate font-semibold uppercase">Sanati</span>
|
|
<span className="truncate text-xs">Pro</span>
|
|
</div>
|
|
</a>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
{/* Platform Nav */}
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip="Dashboard"
|
|
isActive={pathname === "/dashboard"}
|
|
>
|
|
<Link href="/dashboard">
|
|
<Terminal />
|
|
<span>Dashboard</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip="Opportunities"
|
|
isActive={pathname === "/opportunities"}
|
|
>
|
|
<Link href="/opportunities">
|
|
<Target />
|
|
<span>Opportunities</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip="Settings"
|
|
isActive={pathname === "/settings"}
|
|
>
|
|
<Link href="/settings">
|
|
<Settings2 />
|
|
<span>Settings</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip="Help"
|
|
isActive={pathname === "/help"}
|
|
>
|
|
<Link href="/help">
|
|
<HelpCircle />
|
|
<span>Help</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
|
|
{/* Projects (Simple List for now, can be switcher) */}
|
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{projects?.map((project) => (
|
|
<SidebarMenuItem key={project._id}>
|
|
<SidebarMenuButton
|
|
onClick={() => setSelectedProjectId(project._id)}
|
|
isActive={selectedProjectId === project._id}
|
|
>
|
|
<Frame className="text-muted-foreground" />
|
|
<span>{project.name}</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
))}
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton className="text-muted-foreground">
|
|
<Plus />
|
|
<span>Create Project</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
|
|
{/* Data Sources Config */}
|
|
{selectedProjectId && (
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>
|
|
Active Data Sources
|
|
<span className="ml-2 text-xs font-normal text-muted-foreground">({selectedProject?.name})</span>
|
|
</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<div className="flex flex-col gap-2 p-2">
|
|
{(!dataSources || dataSources.length === 0) && (
|
|
<div className="text-sm text-muted-foreground pl-2">No data sources yet.</div>
|
|
)}
|
|
{dataSources?.map((source) => (
|
|
<div key={source._id} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={source._id}
|
|
checked={selectedSourceIds.includes(source._id)}
|
|
onCheckedChange={(checked) => handleToggle(source._id, checked === true)}
|
|
/>
|
|
<Label htmlFor={source._id} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 truncate cursor-pointer">
|
|
{source.name || source.url}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setIsAdding(true)}
|
|
>
|
|
Add Data Source
|
|
</Button>
|
|
</div>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
)}
|
|
</SidebarContent>
|
|
<SidebarFooter>
|
|
<NavUser user={{
|
|
name: "User",
|
|
email: "user@example.com",
|
|
avatar: ""
|
|
}} />
|
|
</SidebarFooter>
|
|
<Dialog open={isAdding} onOpenChange={setIsAdding}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Add Data Source</DialogTitle>
|
|
<DialogDescription>
|
|
Add a website to analyze for this project.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="sourceUrl">Website URL</Label>
|
|
<Input
|
|
id="sourceUrl"
|
|
placeholder="https://example.com"
|
|
value={sourceUrl}
|
|
onChange={(event) => setSourceUrl(event.target.value)}
|
|
disabled={isSubmittingSource}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="sourceName">Name (optional)</Label>
|
|
<Input
|
|
id="sourceName"
|
|
placeholder="Product name"
|
|
value={sourceName}
|
|
onChange={(event) => setSourceName(event.target.value)}
|
|
disabled={isSubmittingSource}
|
|
/>
|
|
</div>
|
|
{sourceError && (
|
|
<div className="text-sm text-destructive">{sourceError}</div>
|
|
)}
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setIsAdding(false)} disabled={isSubmittingSource}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleAddSource} disabled={isSubmittingSource}>
|
|
{isSubmittingSource ? "Analyzing..." : "Add Source"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Sidebar>
|
|
)
|
|
}
|