diff --git a/frontend/app/api/projects/[projectId]/queues/[queueId]/push/route.ts b/frontend/app/api/projects/[projectId]/queues/[queueId]/push/route.ts index 6fbaa61b0..2b6d433e0 100644 --- a/frontend/app/api/projects/[projectId]/queues/[queueId]/push/route.ts +++ b/frontend/app/api/projects/[projectId]/queues/[queueId]/push/route.ts @@ -1,27 +1,26 @@ -import { pushQueueItems, PushQueueItemsRequestSchema } from "@/lib/actions/queue"; +import { prettifyError, ZodError } from "zod/v4"; + +import { pushQueueItems } from "@/lib/actions/queue"; export async function POST(request: Request, props: { params: Promise<{ projectId: string; queueId: string }> }) { const params = await props.params; try { const body = await request.json(); - const result = PushQueueItemsRequestSchema.safeParse(body); - - if (!result.success) { - return Response.json({ error: "Invalid request body", details: result.error }, { status: 400 }); - } - const newQueueItems = await pushQueueItems({ queueId: params.queueId, - items: result.data, + items: body, }); return Response.json(newQueueItems); } catch (error) { - console.error("Error pushing queue items:", error); - if (error instanceof Error && error.message === "Failed to push items to queue") { - return Response.json({ error: "Failed to push items to queue" }, { status: 500 }); + if (error instanceof ZodError) { + return Response.json({ error: prettifyError(error) }, { status: 400 }); } - return Response.json({ error: "Internal server error" }, { status: 500 }); + + return Response.json( + { error: error instanceof Error ? error.message : '"Failed to push items to queue" Please try again.' }, + { status: 500 } + ); } } diff --git a/frontend/app/invitations/page.tsx b/frontend/app/invitations/page.tsx index d041857dd..c7af3b9e5 100644 --- a/frontend/app/invitations/page.tsx +++ b/frontend/app/invitations/page.tsx @@ -82,10 +82,8 @@ export default async function InvitationsPage(props: { return notFound(); } - console.log("passed token"); const decoded = verifyToken(token); - console.log("workspace here", decoded); const workspace = await db.query.workspaces.findFirst({ where: eq(workspaces.id, decoded.workspaceId), }); diff --git a/frontend/components/queue/queue.tsx b/frontend/components/queue/queue.tsx index 835445e9f..c58cbd55a 100644 --- a/frontend/components/queue/queue.tsx +++ b/frontend/components/queue/queue.tsx @@ -51,7 +51,6 @@ function QueueInner() { annotationSchema: state.annotationSchema, })); - const states = useMemo(() => { const isEmpty = !currentItem || currentItem.count === 0; const isFirstItem = currentItem?.position === 1; @@ -77,6 +76,10 @@ function QueueInner() { if (get(currentItem.metadata, "source") === "span") { return `/project/${projectId}/traces?traceId=${get(currentItem.metadata, "traceId")}&spanId=${get(currentItem.metadata, "id")}`; } + + if (get(currentItem.metadata, "source") === "sql") { + return `/project/${projectId}/sql/${get(currentItem.metadata, "id")}`; + } return `/project/${projectId}/labeling-queues/${storeQueue?.id}`; }, [currentItem, projectId, storeQueue?.id]); diff --git a/frontend/components/sql/export-sql-dialog.tsx b/frontend/components/sql/export-sql-dialog.tsx index a4d4dd9c8..a57bb4efb 100644 --- a/frontend/components/sql/export-sql-dialog.tsx +++ b/frontend/components/sql/export-sql-dialog.tsx @@ -9,11 +9,13 @@ import { useSensor, useSensors, } from "@dnd-kit/core"; -import { ChevronDown, Database, Loader2 } from "lucide-react"; +import { ChevronDown, Database, Loader2, Pen, Plus } from "lucide-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { PropsWithChildren, useCallback, useState } from "react"; +import useSWR from "swr"; +import CreateQueueDialog from "@/components/queues/create-queue-dialog"; import { CategoryDropZone, ColumnCategory } from "@/components/sql/dnd-components"; import { Button } from "@/components/ui/button"; import DatasetSelect from "@/components/ui/dataset-select"; @@ -25,8 +27,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dataset } from "@/lib/dataset/types"; import { useToast } from "@/lib/hooks/use-toast"; +import { LabelingQueue } from "@/lib/queue/types"; +import { PaginatedResponse } from "@/lib/types"; +import { swrFetcher } from "@/lib/utils"; import ExportJobDialog from "./export-job-dialog"; @@ -249,6 +255,239 @@ function ExportDatasetDialog({ results, children }: PropsWithChildren>) { + const { projectId, id } = useParams(); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [selectedQueue, setSelectedQueue] = useState(""); + const [isExporting, setIsExporting] = useState(false); + const [columnsByCategory, setColumnsByCategory] = useState>({ + data: [], + target: [], + metadata: [], + }); + const { toast } = useToast(); + + const { data: labelingQueues, isLoading: isQueuesLoading } = useSWR>( + `/api/projects/${projectId}/queues`, + swrFetcher + ); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 12, + }, + }), + useSensor(KeyboardSensor) + ); + + const handleDialogOpen = (open: boolean) => { + if (open && results && results.length > 0) { + const allColumns = Object.keys(results[0]); + setColumnsByCategory({ + data: allColumns, + target: [], + metadata: [], + }); + } else { + setSelectedQueue(""); + } + + setIsExportDialogOpen(open); + }; + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const activeData = active.data.current as { column: string; category: ColumnCategory }; + const sourceCategory = activeData.category; + const columnName = activeData.column; + const targetCategory = over.id as ColumnCategory; + + if ( + sourceCategory !== targetCategory && + (targetCategory === "data" || targetCategory === "target" || targetCategory === "metadata") + ) { + setColumnsByCategory((prev) => { + const sourceColumns = prev[sourceCategory].filter((col) => col !== columnName); + + const targetColumns = [...prev[targetCategory]]; + if (!targetColumns.includes(columnName)) { + targetColumns.push(columnName); + } + + return { + ...prev, + [sourceCategory]: sourceColumns, + [targetCategory]: targetColumns, + }; + }); + } + } + }, []); + + const removeColumnFromCategory = useCallback((column: string, category: ColumnCategory) => { + setColumnsByCategory((prev) => ({ + ...prev, + [category]: prev[category].filter((c) => c !== column), + })); + }, []); + + const exportToQueue = useCallback(async () => { + if (!selectedQueue || !results || results.length === 0) return; + + setIsExporting(true); + + try { + const queueItems = results.map((row, index) => { + const payload = { + data: {} as Record, + target: {} as Record, + metadata: {} as Record, + }; + + columnsByCategory.data.forEach((key) => { + payload.data[key] = row[key]; + }); + + columnsByCategory.target.forEach((key) => { + payload.target[key] = row[key]; + }); + + columnsByCategory.metadata.forEach((key) => { + payload.metadata[key] = row[key]; + }); + + return { + createdAt: new Date(Date.now() + index * 1000).toISOString(), + payload, + metadata: { + source: "sql" as const, + id, + }, + }; + }); + + const res = await fetch(`/api/projects/${projectId}/queues/${selectedQueue}/push`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(queueItems), + }); + + if (!res.ok) { + throw new Error("Failed to export data to labeling queue"); + } + + toast({ + title: `Exported to labeling queue`, + description: ( + + Successfully exported {queueItems.length} results to labeling queue.{" "} + + Go to queue. + + + ), + }); + + setIsExportDialogOpen(false); + } catch (err) { + toast({ + title: "Failed to export data to labeling queue", + description: err instanceof Error ? err.message : "An unexpected error occurred", + variant: "destructive", + }); + } finally { + setIsExporting(false); + } + }, [ + columnsByCategory.data, + columnsByCategory.metadata, + columnsByCategory.target, + projectId, + results, + selectedQueue, + toast, + ]); + + if (!results || results.length === 0) { + return null; + } + + return ( + + {children} + + + Export SQL Results to Labeling Queue + + +
+
+ + +
+
+
+ +

Drag and drop columns between categories

+
+ +
+ removeColumnFromCategory(column, "data")} + /> + removeColumnFromCategory(column, "target")} + /> + removeColumnFromCategory(column, "metadata")} + /> +
+
+
+
+
+
+ ); +} + export default function ExportSqlDialog({ results, sqlQuery, children }: PropsWithChildren) { return ( @@ -256,7 +495,7 @@ export default function ExportSqlDialog({ results, sqlQuery, children }: PropsWi {children || ( )} @@ -268,6 +507,12 @@ export default function ExportSqlDialog({ results, sqlQuery, children }: PropsWi Export to Dataset + + e.preventDefault()}> + + Export to Labeling Queue + + e.preventDefault()}> diff --git a/frontend/lib/actions/queue/index.ts b/frontend/lib/actions/queue/index.ts index d83751b1a..8aee908de 100644 --- a/frontend/lib/actions/queue/index.ts +++ b/frontend/lib/actions/queue/index.ts @@ -24,7 +24,7 @@ export const PushQueueItemSchema = z.object({ metadata: z.any(), }), metadata: z.object({ - source: z.enum(["span", "datapoint"]), + source: z.enum(["span", "datapoint", "sql"]), datasetId: z.string().optional(), traceId: z.string().optional(), id: z.string(),