From 4e58a86cf80f8e2792054cc3b36683d71cfcb7b4 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 18:33:31 -0300 Subject: [PATCH] =?UTF-8?q?fix(batch):=20round-2=20polish=20=E2=80=94=20TS?= =?UTF-8?q?2305=20unmask,=20a11y=20dialog,=20BOM=20stripping,=2024h=20wind?= =?UTF-8?q?ow,=20Provider=20column,=20race=20guard,=20banner=20toast,=20i1?= =?UTF-8?q?8n=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 13 fixes from independent round-2 code review: - B-1: Fix FileRecord wrong import in batch-utils.ts (TS2305 masked by limited typecheck-core scope); include batch-utils.ts + 5 lib/batches/* in tsconfig.typecheck-core.json so future regressions are caught - A-2: role="dialog" + aria-modal + aria-labelledby on NewBatchWizard + BatchDetailModal panels (a11y consistency with UploadFileModal) - A-3: "Janela de conclusão de 24h" line in CostEstimateStep (spec §5 explicit) - A-7: "campos obrigatórios válidos" appended to JsonlValidationStep success summary (spec §5) - A-9 + B-7: 18 hardcoded UI strings → i18n keys (Size, Refresh/Refreshing, Remove, Uploading, Reading file, Ready, Large file warning, JSONL generated, etc.) - B-4: Strip UTF-8 BOM in validateJsonl + csvToJsonl (Windows-saved files no longer fail "invalid JSON" cryptically) - B-5: Remove dead exports WizardStep + BatchProviderConfig from types.ts - A-1: Add Provider column with derived heuristic (gpt-/o1-/o3-/text-embedding-/dall-e- → OpenAI; claude- → Anthropic; gemini → Gemini; else "—") in BatchListTab (BatchRecord has no provider field, so derivation is display-only) - A-5: Enter key advances wizard step via data-wizard-next button trigger (skip when focus in INPUT/TEXTAREA/SELECT) - A-6: Auto-dismiss "Batch {id} criado" banner on /batch page after wizard completes; consumes onCreated id (was discarded as _id) - B-2: Race guard in InputStep.processFile (early-return if isReading) + drop zone pointer-events-none while reading - C-tests: 6 new regression tests covering body=[] Array.isArray guard, BOM stripping, alias-match pricing path, (partial) suffix on expired_with_failures, -50% inline badge on Cost column, Provider column derivation per family Side fix: narrow Record child navigation in csvToJsonl.ts:135 to silence TS2322 once file is in typecheck scope. 22 new i18n keys (filesListSizeColumn, batchListProviderColumn/Unknown/BatchCreated/Dismiss/Refreshing/Refresh, uploadFileModalRemove/Uploading, wizardInputReading/Ready/LargeFileLabel/CsvJsonlReady/LargeFileWarning, wizardCostWindow24h, wizardValidationFieldsOk) added in en.json + pt-BR.json and propagated to 40 locales via fill-missing-from-en.mjs. --- .../dashboard/batch/BatchDetailModal.tsx | 9 +++- .../dashboard/batch/BatchListTab.tsx | 21 +++++++- .../dashboard/batch/FilesListTab.tsx | 2 +- .../dashboard/batch/batch-utils.ts | 3 +- .../batch/components/NewBatchWizard.tsx | 28 +++++++++-- .../batch/components/UploadFileModal.tsx | 4 +- .../components/wizard/CostEstimateStep.tsx | 9 ++++ .../batch/components/wizard/InputStep.tsx | 22 +++++---- .../components/wizard/JsonlValidationStep.tsx | 5 +- src/app/(dashboard)/dashboard/batch/page.tsx | 35 ++++++++++++-- src/i18n/messages/ar.json | 18 ++++++- src/i18n/messages/az.json | 18 ++++++- src/i18n/messages/bg.json | 18 ++++++- src/i18n/messages/bn.json | 18 ++++++- src/i18n/messages/cs.json | 18 ++++++- src/i18n/messages/da.json | 18 ++++++- src/i18n/messages/de.json | 18 ++++++- src/i18n/messages/en.json | 16 +++++++ src/i18n/messages/es.json | 18 ++++++- src/i18n/messages/fa.json | 18 ++++++- src/i18n/messages/fi.json | 18 ++++++- src/i18n/messages/fr.json | 18 ++++++- src/i18n/messages/gu.json | 18 ++++++- src/i18n/messages/he.json | 18 ++++++- src/i18n/messages/hi.json | 18 ++++++- src/i18n/messages/hu.json | 18 ++++++- src/i18n/messages/id.json | 18 ++++++- src/i18n/messages/in.json | 18 ++++++- src/i18n/messages/it.json | 18 ++++++- src/i18n/messages/ja.json | 18 ++++++- src/i18n/messages/ko.json | 18 ++++++- src/i18n/messages/mr.json | 18 ++++++- src/i18n/messages/ms.json | 18 ++++++- src/i18n/messages/nl.json | 18 ++++++- src/i18n/messages/no.json | 18 ++++++- src/i18n/messages/phi.json | 18 ++++++- src/i18n/messages/pl.json | 18 ++++++- src/i18n/messages/pt-BR.json | 16 +++++++ src/i18n/messages/pt.json | 18 ++++++- src/i18n/messages/ro.json | 18 ++++++- src/i18n/messages/ru.json | 18 ++++++- src/i18n/messages/sk.json | 18 ++++++- src/i18n/messages/sv.json | 18 ++++++- src/i18n/messages/sw.json | 18 ++++++- src/i18n/messages/ta.json | 18 ++++++- src/i18n/messages/te.json | 18 ++++++- src/i18n/messages/th.json | 18 ++++++- src/i18n/messages/tr.json | 18 ++++++- src/i18n/messages/uk-UA.json | 18 ++++++- src/i18n/messages/ur.json | 18 ++++++- src/i18n/messages/vi.json | 18 ++++++- src/i18n/messages/zh-CN.json | 18 ++++++- src/lib/batches/csvToJsonl.ts | 9 ++-- src/lib/batches/types.ts | 8 ---- src/lib/batches/validateJsonl.ts | 4 +- .../dashboard/batch/list-regression.test.tsx | 48 +++++++++++++++++++ tests/unit/lib/batches/costEstimator.test.ts | 19 ++++++++ tests/unit/lib/batches/validateJsonl.test.ts | 27 +++++++++++ tsconfig.typecheck-core.json | 7 +++ 59 files changed, 934 insertions(+), 78 deletions(-) diff --git a/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx b/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx index c760eb332..e3729a408 100644 --- a/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx +++ b/src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx @@ -182,7 +182,12 @@ export default function BatchDetailModal({ batch, files, onClose, onActionDone } /> {/* Panel */} -
+
{/* Header */}
@@ -190,7 +195,7 @@ export default function BatchDetailModal({ batch, files, onClose, onActionDone } pending_actions
-

+

Batch Details

diff --git a/src/app/(dashboard)/dashboard/batch/BatchListTab.tsx b/src/app/(dashboard)/dashboard/batch/BatchListTab.tsx index 7479a85c5..6b20d1326 100644 --- a/src/app/(dashboard)/dashboard/batch/BatchListTab.tsx +++ b/src/app/(dashboard)/dashboard/batch/BatchListTab.tsx @@ -94,6 +94,17 @@ const STATUS_STYLES: Record = { expired_with_failures: "bg-orange-500/15 text-orange-400 border-orange-500/25", }; +// A-1 — Derive Provider label from model id (no provider field on BatchRecord). +// Heuristic-only display; no filter side-effect. Returns "—" for unknown shapes. +function deriveProvider(model: string | null | undefined): string { + if (!model) return "—"; + const lower = model.toLowerCase(); + if (lower.startsWith("gpt-") || lower.startsWith("o1") || lower.startsWith("o3") || lower.startsWith("text-embedding-") || lower.startsWith("dall-e")) return "OpenAI"; + if (lower.startsWith("claude-")) return "Anthropic"; + if (lower.startsWith("gemini")) return "Gemini"; + return "Other"; +} + const STATUS_LABELS: Record = { completed_with_failures: "completed with failures", in_progress_with_failures: "in progress (with failures)", @@ -371,6 +382,9 @@ export default function BatchListTab({ ID + + {t("batchListProviderColumn")} + Endpoint @@ -395,7 +409,7 @@ export default function BatchListTab({ {loading && filtered.length === 0 ? ( - +
Loading… @@ -404,7 +418,7 @@ export default function BatchListTab({ ) : filtered.length === 0 ? ( - + No batches found @@ -447,6 +461,9 @@ export default function BatchListTab({ {batch.id} + + {deriveProvider(batch.model)} + {batch.endpoint} diff --git a/src/app/(dashboard)/dashboard/batch/FilesListTab.tsx b/src/app/(dashboard)/dashboard/batch/FilesListTab.tsx index f47f52db5..990250953 100644 --- a/src/app/(dashboard)/dashboard/batch/FilesListTab.tsx +++ b/src/app/(dashboard)/dashboard/batch/FilesListTab.tsx @@ -176,7 +176,7 @@ export default function FilesListTab({ Purpose - Size + {t("filesListSizeColumn")} {t("filesListUsedByColumn")} diff --git a/src/app/(dashboard)/dashboard/batch/batch-utils.ts b/src/app/(dashboard)/dashboard/batch/batch-utils.ts index 84daff2ed..5f63d9f12 100644 --- a/src/app/(dashboard)/dashboard/batch/batch-utils.ts +++ b/src/app/(dashboard)/dashboard/batch/batch-utils.ts @@ -1,4 +1,5 @@ -import { BatchRecord, FileRecord } from "@/lib/db/batches"; +import { BatchRecord } from "@/lib/db/batches"; +import { FileRecord } from "@/lib/db/files"; export function mapBatchApiToRecord(b: any): BatchRecord { return { diff --git a/src/app/(dashboard)/dashboard/batch/components/NewBatchWizard.tsx b/src/app/(dashboard)/dashboard/batch/components/NewBatchWizard.tsx index af8ab5664..836b184e0 100644 --- a/src/app/(dashboard)/dashboard/batch/components/NewBatchWizard.tsx +++ b/src/app/(dashboard)/dashboard/batch/components/NewBatchWizard.tsx @@ -155,10 +155,24 @@ export default function NewBatchWizard({ const t = useTranslations("common"); const [state, dispatch] = useReducer(reducer, initialState); - // Escape key handler + // Escape closes wizard; Enter advances to next step when allowed (A-5). + // Skip Enter when focus is inside form controls so it doesn't interfere with typing. useEffect(() => { function onKeyDown(e: KeyboardEvent) { - if (e.key === "Escape" && !state.creating) onClose(); + if (state.creating) return; + if (e.key === "Escape") { + onClose(); + return; + } + if (e.key === "Enter") { + const tag = (e.target as HTMLElement | null)?.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; + // canGoNext lives below; compute lazily via reading the disabled state of the Next button + const nextBtn = document.querySelector( + 'button[data-wizard-next="true"]' + ); + if (nextBtn && !nextBtn.disabled) nextBtn.click(); + } } document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); @@ -292,11 +306,16 @@ export default function NewBatchWizard({ /> {/* Panel */} -
+
{/* Header */}
-

{t("wizardTitle")}

+

{t("wizardTitle")}

)} @@ -229,7 +229,7 @@ export default function UploadFileModal({ onClose, onUploaded }: Props) { {uploading ? ( <> - Uploading… + {t("uploadFileModalUploading")} ) : ( <> diff --git a/src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx b/src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx index 3540f0e53..0e53bc8d1 100644 --- a/src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx +++ b/src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx @@ -103,6 +103,15 @@ export default function CostEstimateStep({ {estimate.estimatedOutputTokens.toLocaleString()} output tok
+ + {/* Completion window — spec §5 "janela 24h" (A-3) */} +
+ Window + + schedule + {t("wizardCostWindow24h")} + +
)} diff --git a/src/app/(dashboard)/dashboard/batch/components/wizard/InputStep.tsx b/src/app/(dashboard)/dashboard/batch/components/wizard/InputStep.tsx index 88bf4be79..a777dea7a 100644 --- a/src/app/(dashboard)/dashboard/batch/components/wizard/InputStep.tsx +++ b/src/app/(dashboard)/dashboard/batch/components/wizard/InputStep.tsx @@ -47,6 +47,8 @@ export default function InputStep({ input, onChange, destination }: InputStepPro } async function processFile(file: File) { + // B-2 race guard — ignore concurrent drops while a read is in flight + if (isReading) return; const expectedExt = isJsonl ? ".jsonl" : ".csv"; if (!file.name.toLowerCase().endsWith(expectedExt)) { // Soft warning — don't block, let validation catch it @@ -130,30 +132,33 @@ export default function InputStep({ input, onChange, destination }: InputStepPro
- {/* Drop zone */} + {/* Drop zone — disabled while reading to prevent race (B-2) */}
fileInputRef.current?.click()} + onClick={() => !isReading && fileInputRef.current?.click()} onKeyDown={(e) => { + if (isReading) return; if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }} - className={`rounded-xl border-2 border-dashed p-8 flex flex-col items-center gap-3 cursor-pointer transition-colors + aria-disabled={isReading} + className={`rounded-xl border-2 border-dashed p-8 flex flex-col items-center gap-3 transition-colors + ${isReading ? "cursor-wait opacity-60 pointer-events-none" : "cursor-pointer"} ${isDragging ? "border-[var(--color-accent)] bg-[var(--color-accent)]/5" : "border-[var(--color-border)] hover:border-[var(--color-accent)]/50"}`} > upload_file {isReading ? ( - Reading file… + {t("wizardInputReading")} ) : hasFile ? (
{input.fileName} - {isLargeFile ? "Large file — validation by sampling" : "Ready"} + {isLargeFile ? t("wizardInputLargeFileLabel") : t("wizardInputReady")}
) : ( @@ -171,8 +176,7 @@ export default function InputStep({ input, onChange, destination }: InputStepPro {/* Large file warning */} {isLargeFile && (
- Large file detected — validation will run by sampling (first 5 MB + last 100 KB). Full - validation happens server-side. + {t("wizardInputLargeFileWarning")}
)} @@ -187,7 +191,7 @@ export default function InputStep({ input, onChange, destination }: InputStepPro onJsonlReady={handleJsonlReady} /> {csvMappingReady && ( -

JSONL generated — ready to validate.

+

{t("wizardInputCsvJsonlReady")}

)}
)} diff --git a/src/app/(dashboard)/dashboard/batch/components/wizard/JsonlValidationStep.tsx b/src/app/(dashboard)/dashboard/batch/components/wizard/JsonlValidationStep.tsx index f9e11be88..061704a04 100644 --- a/src/app/(dashboard)/dashboard/batch/components/wizard/JsonlValidationStep.tsx +++ b/src/app/(dashboard)/dashboard/batch/components/wizard/JsonlValidationStep.tsx @@ -64,14 +64,15 @@ export default function JsonlValidationStep({ return (
- {/* OK / Error banner */} + {/* OK / Error banner — spec §5 "campos OK" appended on success (A-7) */} {result.ok ? (
check_circle
{t("wizardValidationOk")} - {result.totalLines} lines · {result.uniqueCustomIds} unique custom_ids + {result.totalLines} lines · {result.uniqueCustomIds} unique custom_ids ·{" "} + {t("wizardValidationFieldsOk")}
diff --git a/src/app/(dashboard)/dashboard/batch/page.tsx b/src/app/(dashboard)/dashboard/batch/page.tsx index 0e94e47cd..996ac551c 100644 --- a/src/app/(dashboard)/dashboard/batch/page.tsx +++ b/src/app/(dashboard)/dashboard/batch/page.tsx @@ -33,10 +33,18 @@ export default function BatchPage() { const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [showWizard, setShowWizard] = useState(false); + const [createdBanner, setCreatedBanner] = useState(null); const [providers, setProviders] = useState>( [], ); + // Auto-dismiss "batch created" banner after 5s (A-6) + useEffect(() => { + if (!createdBanner) return; + const id = setTimeout(() => setCreatedBanner(null), 5000); + return () => clearTimeout(id); + }, [createdBanner]); + const [batchesHasMore, setBatchesHasMore] = useState(false); const [batchesLastId, setBatchesLastId] = useState(null); const bottomRefBatches = useRef(null); @@ -241,6 +249,26 @@ export default function BatchPage() { {/* Concept card (F3) */} + {/* "Batch created" success banner (A-6) — auto-dismiss 5s */} + {createdBanner && ( +
+
+ check_circle + {t("batchListBatchCreated", { id: createdBanner })} +
+ +
+ )} + {/* Toolbar: auto-refresh indicator + Refresh + New batch */}
@@ -258,7 +286,7 @@ export default function BatchPage() { disabled:opacity-50 disabled:cursor-not-allowed" > refresh - {loading ? "Refreshing…" : "Refresh"} + {loading ? t("batchListRefreshing") : t("batchListRefresh")}