feat: Implement analysis job tracking with progress timeline and enhanced data source status management.
This commit is contained in:
@@ -7,10 +7,13 @@ import {
|
||||
Command,
|
||||
Frame,
|
||||
HelpCircle,
|
||||
Settings,
|
||||
Settings2,
|
||||
Terminal,
|
||||
Target,
|
||||
Plus
|
||||
Plus,
|
||||
ArrowUpRight,
|
||||
ChevronsUpDown
|
||||
} from "lucide-react"
|
||||
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
@@ -26,6 +29,14 @@ import {
|
||||
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"
|
||||
@@ -34,19 +45,49 @@ 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<typeof Sidebar>) {
|
||||
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<string | null>(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<string | null>(null);
|
||||
const [editingProjectName, setEditingProjectName] = React.useState("");
|
||||
const [editingProjectDefault, setEditingProjectDefault] = React.useState(false);
|
||||
const [editingProjectError, setEditingProjectError] = React.useState<string | null>(null);
|
||||
const [isSubmittingEdit, setIsSubmittingEdit] = React.useState(false);
|
||||
const [sourceUrl, setSourceUrl] = React.useState("");
|
||||
const [sourceName, setSourceName] = React.useState("");
|
||||
const [sourceError, setSourceError] = React.useState<string | null>(null);
|
||||
const [sourceNotice, setSourceNotice] = React.useState<string | null>(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<string | null>(null);
|
||||
const [pendingProjectId, setPendingProjectId] = React.useState<string | null>(null);
|
||||
const [pendingJobId, setPendingJobId] = React.useState<string | null>(null);
|
||||
const analysisJob = useQuery(
|
||||
api.analysisJobs.getById,
|
||||
pendingJobId ? { jobId: pendingJobId as any } : "skip"
|
||||
);
|
||||
|
||||
// Set default selected project
|
||||
React.useEffect(() => {
|
||||
@@ -67,6 +108,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
|
||||
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;
|
||||
@@ -87,15 +129,27 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
setIsSubmittingSource(true);
|
||||
|
||||
try {
|
||||
const { sourceId, projectId } = await addDataSource({
|
||||
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: sourceId,
|
||||
dataSourceId: result.sourceId,
|
||||
analysisStatus: "pending",
|
||||
lastError: undefined,
|
||||
lastAnalyzedAt: undefined,
|
||||
@@ -104,14 +158,25 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const response = await fetch("/api/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: sourceUrl }),
|
||||
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: sourceId,
|
||||
dataSourceId: result.sourceId,
|
||||
analysisStatus: "failed",
|
||||
lastError: data.error || "Analysis failed",
|
||||
lastAnalyzedAt: Date.now(),
|
||||
@@ -120,13 +185,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
}
|
||||
|
||||
await createAnalysis({
|
||||
projectId,
|
||||
dataSourceId: sourceId,
|
||||
projectId: result.projectId,
|
||||
dataSourceId: result.sourceId,
|
||||
analysis: data.data,
|
||||
});
|
||||
|
||||
await updateDataSourceStatus({
|
||||
dataSourceId: sourceId,
|
||||
dataSourceId: result.sourceId,
|
||||
analysisStatus: "completed",
|
||||
lastError: undefined,
|
||||
lastAnalyzedAt: Date.now(),
|
||||
@@ -134,6 +199,14 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
|
||||
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.");
|
||||
@@ -142,22 +215,138 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<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>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton size="lg">
|
||||
<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">{selectedProjectName}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">Projects</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="start" className="w-64">
|
||||
<DropdownMenuLabel>Switch project</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{projects?.map((project) => (
|
||||
<DropdownMenuItem
|
||||
key={project._id}
|
||||
className="justify-between"
|
||||
onSelect={(event) => {
|
||||
if (event.defaultPrevented) return
|
||||
setSelectedProjectId(project._id)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Frame className="text-muted-foreground" />
|
||||
<span className="truncate">{project.name}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-project-settings
|
||||
className="rounded-md p-1 text-muted-foreground hover:text-foreground"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation();
|
||||
setEditingProjectId(project._id);
|
||||
setEditingProjectName(project.name);
|
||||
setEditingProjectDefault(project.isDefault);
|
||||
setEditingProjectError(null);
|
||||
setIsEditingProject(true);
|
||||
}}
|
||||
aria-label={`Project settings for ${project.name}`}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={() => setIsCreatingProject(true)}>
|
||||
<Plus />
|
||||
Create Project
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
@@ -219,32 +408,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</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>
|
||||
@@ -258,15 +421,30 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<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
|
||||
key={source._id}
|
||||
className="flex items-center justify-between gap-2 rounded-md px-2 py-1 hover:bg-muted/40"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Checkbox
|
||||
id={source._id}
|
||||
checked={selectedSourceIds.includes(source._id)}
|
||||
onCheckedChange={(checked) => handleToggle(source._id, checked === true)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={source._id}
|
||||
className="truncate text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
{source.name || source.url}
|
||||
</Label>
|
||||
</div>
|
||||
<Link
|
||||
href={`/data-sources/${source._id}`}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Details
|
||||
<ArrowUpRight className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
@@ -282,13 +460,45 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={{
|
||||
name: "User",
|
||||
email: "user@example.com",
|
||||
avatar: ""
|
||||
}} />
|
||||
{currentUser && (currentUser.name || currentUser.email) && (
|
||||
<NavUser
|
||||
user={{
|
||||
name: currentUser.name || currentUser.email || "",
|
||||
email: currentUser.email || "",
|
||||
avatar: currentUser.image || "",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SidebarFooter>
|
||||
<Dialog open={isAdding} onOpenChange={setIsAdding}>
|
||||
<Dialog
|
||||
open={isAdding}
|
||||
onOpenChange={(open) => {
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Data Source</DialogTitle>
|
||||
@@ -297,35 +507,224 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</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>
|
||||
{!manualMode && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{manualMode && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="manualProductName">Product Name</Label>
|
||||
<Input
|
||||
id="manualProductName"
|
||||
value={manualProductName}
|
||||
onChange={(event) => setManualProductName(event.target.value)}
|
||||
disabled={isSubmittingSource}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="manualDescription">Description</Label>
|
||||
<Textarea
|
||||
id="manualDescription"
|
||||
value={manualDescription}
|
||||
onChange={(event) => setManualDescription(event.target.value)}
|
||||
disabled={isSubmittingSource}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="manualFeatures">Key Features (one per line)</Label>
|
||||
<Textarea
|
||||
id="manualFeatures"
|
||||
value={manualFeatures}
|
||||
onChange={(event) => setManualFeatures(event.target.value)}
|
||||
disabled={isSubmittingSource}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{sourceNotice && (
|
||||
<div className="text-sm text-muted-foreground">{sourceNotice}</div>
|
||||
)}
|
||||
{sourceError && (
|
||||
<div className="text-sm text-destructive">{sourceError}</div>
|
||||
)}
|
||||
{analysisJob?.timeline?.length ? (
|
||||
<AnalysisTimeline items={analysisJob.timeline} />
|
||||
) : null}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsAdding(false)} disabled={isSubmittingSource}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAdding(false)}
|
||||
disabled={isSubmittingSource}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAddSource} disabled={isSubmittingSource}>
|
||||
{isSubmittingSource ? "Analyzing..." : "Add Source"}
|
||||
{manualMode ? (
|
||||
<Button onClick={handleManualAnalyze} disabled={isSubmittingSource}>
|
||||
{isSubmittingSource ? "Analyzing..." : "Analyze Manually"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleAddSource} disabled={isSubmittingSource}>
|
||||
{isSubmittingSource ? "Analyzing..." : "Add Source"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isEditingProject} onOpenChange={setIsEditingProject}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Project Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the project name and default status.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="editProjectName">Project Name</Label>
|
||||
<Input
|
||||
id="editProjectName"
|
||||
value={editingProjectName}
|
||||
onChange={(event) => setEditingProjectName(event.target.value)}
|
||||
disabled={isSubmittingEdit}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="editProjectDefault"
|
||||
checked={editingProjectDefault}
|
||||
onCheckedChange={(checked) => setEditingProjectDefault(checked === true)}
|
||||
disabled={isSubmittingEdit}
|
||||
/>
|
||||
<Label htmlFor="editProjectDefault">Set as default</Label>
|
||||
</div>
|
||||
{editingProjectError && (
|
||||
<div className="text-sm text-destructive">{editingProjectError}</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingProject(false)}
|
||||
disabled={isSubmittingEdit}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!editingProjectId) return;
|
||||
if (!editingProjectName.trim()) {
|
||||
setEditingProjectError("Project name is required.");
|
||||
return;
|
||||
}
|
||||
setIsSubmittingEdit(true);
|
||||
setEditingProjectError(null);
|
||||
try {
|
||||
await updateProject({
|
||||
projectId: editingProjectId as any,
|
||||
name: editingProjectName.trim(),
|
||||
isDefault: editingProjectDefault,
|
||||
});
|
||||
setIsEditingProject(false);
|
||||
} catch (err: any) {
|
||||
setEditingProjectError(err?.message || "Failed to update project.");
|
||||
} finally {
|
||||
setIsSubmittingEdit(false);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmittingEdit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isCreatingProject} onOpenChange={setIsCreatingProject}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new project for a separate product or workflow.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
value={projectName}
|
||||
onChange={(event) => setProjectName(event.target.value)}
|
||||
disabled={isSubmittingProject}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
id="projectDefault"
|
||||
checked={projectDefault}
|
||||
onCheckedChange={(checked) => setProjectDefault(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="projectDefault">Make this the default project</Label>
|
||||
</div>
|
||||
{projectError && (
|
||||
<div className="text-sm text-destructive">{projectError}</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsCreatingProject(false)}
|
||||
disabled={isSubmittingProject}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!projectName.trim()) {
|
||||
setProjectError("Project name is required.");
|
||||
return;
|
||||
}
|
||||
setProjectError(null);
|
||||
setIsSubmittingProject(true);
|
||||
try {
|
||||
const projectId = await createProject({
|
||||
name: projectName.trim(),
|
||||
isDefault: projectDefault,
|
||||
});
|
||||
setSelectedProjectId(projectId as any);
|
||||
setProjectName("");
|
||||
setProjectDefault(true);
|
||||
setIsCreatingProject(false);
|
||||
} catch (err: any) {
|
||||
setProjectError(err?.message || "Failed to create project.");
|
||||
} finally {
|
||||
setIsSubmittingProject(false);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmittingProject}
|
||||
>
|
||||
{isSubmittingProject ? "Creating..." : "Create Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user