diff --git a/open-sse/services/batchProcessor.ts b/open-sse/services/batchProcessor.ts index 57ed33ee08..15a2a9b455 100644 --- a/open-sse/services/batchProcessor.ts +++ b/open-sse/services/batchProcessor.ts @@ -299,7 +299,19 @@ async function processBatchItems(batch: BatchRecord, items: BatchRequestItem[]) try { // BATCH-SPECIFIC: Force stream: false — batches don't support SSE responses - const batchItemBody = { ...item.body, stream: false }; + const isChatEndpoint = ![ + "/v1/embeddings", + "/v1/moderations", + "/v1/images/generations", + "/v1/images/edits", + "/v1/videos", + "/v1/videos/generations", + ].includes(item.url); + + const batchItemBody = { + ...item.body, + ...(isChatEndpoint ? { stream: false } : {}), + }; const response = await dispatchBatchApiRequest({ endpoint: item.url, body: batchItemBody, diff --git a/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx b/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx new file mode 100644 index 0000000000..8f3f175f2d --- /dev/null +++ b/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx @@ -0,0 +1,363 @@ +"use client"; + +import { useEffect } from "react"; + +function relativeTime(ts: number): string { + const diffMs = Date.now() - ts * 1000; + const diffSec = Math.round(diffMs / 1000); + if (diffSec < 60) return `${diffSec}s ago`; + const diffMin = Math.round(diffSec / 60); + if (diffMin < 60) return `${diffMin}m ago`; + const diffHr = Math.round(diffMin / 60); + if (diffHr < 24) return `${diffHr}h ago`; + return `${Math.round(diffHr / 24)}d ago`; +} + +interface BatchRecord { + id: string; + endpoint: string; + completionWindow: string; + status: string; + inputFileId: string; + outputFileId?: string | null; + errorFileId?: string | null; + createdAt: number; + inProgressAt?: number | null; + expiresAt?: number | null; + finalizingAt?: number | null; + completedAt?: number | null; + failedAt?: number | null; + expiredAt?: number | null; + cancellingAt?: number | null; + cancelledAt?: number | null; + requestCountsTotal: number; + requestCountsCompleted: number; + requestCountsFailed: number; + metadata?: Record | null; + errors?: unknown | null; + model?: string | null; + usage?: unknown | null; +} + +interface FileRecord { + id: string; + filename: string; + bytes: number; + purpose: string; + status?: string | null; + createdAt: number; +} + +interface BatchDetailModalProps { + batch: BatchRecord; + files: FileRecord[]; + onClose: () => void; +} + +const STATUS_STYLES: Record = { + completed: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25", + completed_with_failures: "bg-red-500/15 text-red-400 border-red-500/25", + failed: "bg-red-500/15 text-red-400 border-red-500/25", + in_progress: "bg-blue-500/15 text-blue-400 border-blue-500/25", + in_progress_with_failures: "bg-orange-500/15 text-orange-400 border-orange-500/25", + finalizing: "bg-violet-500/15 text-violet-400 border-violet-500/25", + finalizing_with_failures: "bg-orange-500/15 text-orange-400 border-orange-500/25", + validating: "bg-yellow-500/15 text-yellow-400 border-yellow-500/25", + cancelling: "bg-orange-500/15 text-orange-400 border-orange-500/25", + cancelled: "bg-gray-500/15 text-gray-400 border-gray-500/25", + cancelled_with_failures: "bg-red-500/15 text-red-400 border-red-500/25", + expired: "bg-gray-500/15 text-gray-400 border-gray-500/25", +}; + +const STATUS_LABELS: Record = { + completed_with_failures: "completed with failures", + in_progress_with_failures: "in progress (with failures)", + finalizing_with_failures: "finalizing (with failures)", + cancelled_with_failures: "cancelled with failures", +}; + +function effectiveStatus(batch: BatchRecord): string { + const hasFailed = (batch.requestCountsFailed ?? 0) > 0; + if (!hasFailed) return batch.status; + const map: Record = { + completed: "completed_with_failures", + in_progress: "in_progress_with_failures", + finalizing: "finalizing_with_failures", + cancelled: "cancelled_with_failures", + }; + return map[batch.status] ?? batch.status; +} + +function StatusBadge({ batch }: { batch: BatchRecord }) { + const key = effectiveStatus(batch); + const cls = STATUS_STYLES[key] ?? "bg-gray-500/15 text-gray-400 border-gray-500/25"; + const label = STATUS_LABELS[key] ?? key.replace(/_/g, " "); + return ( + + {label} + + ); +} + +function Field({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ + {label} + + {value} +
+ ); +} + +function formatTs(ts: number | null | undefined): string { + if (!ts) return "—"; + const d = new Date(ts * 1000); + return d.toLocaleString(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +export default function BatchDetailModal({ batch, files, onClose }: BatchDetailModalProps) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handler); + return () => document.removeEventListener("keydown", handler); + }, [onClose]); + + const total = batch.requestCountsTotal || 0; + const completed = batch.requestCountsCompleted || 0; + const failed = batch.requestCountsFailed || 0; + const donePct = total > 0 ? (completed / total) * 100 : 0; + const failedPct = total > 0 ? (failed / total) * 100 : 0; + const pct = Math.round(donePct + failedPct); + + const inputFile = files.find((f) => f.id === batch.inputFileId); + const outputFile = batch.outputFileId ? files.find((f) => f.id === batch.outputFileId) : null; + const errorFile = batch.errorFileId ? files.find((f) => f.id === batch.errorFileId) : null; + + return ( +
+ {/* Overlay */} +