From 35a34ac34f899998e7066c2b488836026a7bf064 Mon Sep 17 00:00:00 2001
From: ignamiranda <34501347+ignamiranda@users.noreply.github.com>
Date: Sun, 23 Aug 2026 05:45:42 -0400
Subject: [PATCH] Beginner UX: lead batch page with stable outcome header
(#11227)
Validated on the combined batch board (batch-page static test green) + this branch: i18n suites 16/16 with Vietnamese translations of the batch header/step strings added here. Stable outcome header with the 3-step flow; the collapsible concept card stays as advanced detail. Thank you @ignamiranda!
---
src/app/(dashboard)/dashboard/batch/page.tsx | 37 +++++++++-
src/i18n/messages/en.json | 7 ++
src/i18n/messages/vi.json | 9 ++-
tests/unit/batch-page-static.test.ts | 74 ++++++++++++++++++++
4 files changed, 125 insertions(+), 2 deletions(-)
create mode 100644 tests/unit/batch-page-static.test.ts
diff --git a/src/app/(dashboard)/dashboard/batch/page.tsx b/src/app/(dashboard)/dashboard/batch/page.tsx
index 0100b8e948..258d837e17 100644
--- a/src/app/(dashboard)/dashboard/batch/page.tsx
+++ b/src/app/(dashboard)/dashboard/batch/page.tsx
@@ -246,7 +246,42 @@ export default function BatchPage() {
return (
- {/* Concept card (F3) */}
+ {/* Stable outcome-oriented header (replaces the collapsible card as primary orientation) */}
+
+
+
{t("batchConceptTitle")}
+
{t("batchHeaderSubtitle")}
+
+
+ {/* Three-step strip */}
+
+
+ {t("batchStep1")}
+ {t("batchStep1Desc")}
+
+
chevron_right
+
+ {t("batchStep2")}
+ {t("batchStep2Desc")}
+
+
chevron_right
+
+ {t("batchStep3")}
+ {t("batchStep3Desc")}
+
+
+
+ {/* Primary CTA */}
+
+
+
+ {/* Deeper explanation (optional, collapsible) */}
{/* "Batch created" success banner (A-6) — auto-dismiss 5s */}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 7bafe22203..a1b934e50b 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -805,6 +805,13 @@
"batchConceptBenefit50pct": "50% discount on input + output tokens",
"batchConceptAsync24h": "Async with a 24h completion window",
"batchConceptUseCases": "Best for bulk classification, evaluations, and embeddings",
+ "batchHeaderSubtitle": "Run many requests as one job",
+ "batchStep1": "1 · Upload JSONL",
+ "batchStep1Desc": "Add requests",
+ "batchStep2": "2 · Create batch",
+ "batchStep2Desc": "Run job",
+ "batchStep3": "3 · Get results",
+ "batchStep3Desc": "Download output",
"filesConceptTitle": "Batch files",
"filesConceptSubtitle": "JSONL files used by batches: input requests, results, and errors.",
"filesConceptInput": "Input — your JSONL with one request per line",
diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json
index e049342cfb..5830360731 100644
--- a/src/i18n/messages/vi.json
+++ b/src/i18n/messages/vi.json
@@ -974,7 +974,14 @@
"batchFileUsedByCount": "{count, plural, one {# batch} other {# batch}}",
"batchFilePreview": "Bản xem trước",
"batchFilePreviewTruncated": "Hiển thị {shown} dòng đầu tiên (tổng cộng {total} dòng)",
- "batchFileDownloadFull": "Tải xuống toàn bộ tệp"
+ "batchFileDownloadFull": "Tải xuống toàn bộ tệp",
+ "batchHeaderSubtitle": "Chạy nhiều yêu cầu như một job duy nhất",
+ "batchStep1": "1 · Tải lên JSONL",
+ "batchStep1Desc": "Thêm yêu cầu",
+ "batchStep2": "2 · Tạo batch",
+ "batchStep2Desc": "Chạy job",
+ "batchStep3": "3 · Nhận kết quả",
+ "batchStep3Desc": "Tải xuống đầu ra"
},
"disabled": "Đã tắt",
"featureFlagOmnirouteEmergencyFallbackDescription": "Định tuyến các yêu cầu đã hết ngân sách đến nhà cung cấp/mô hình dự phòng khẩn cấp miễn phí.",
diff --git a/tests/unit/batch-page-static.test.ts b/tests/unit/batch-page-static.test.ts
new file mode 100644
index 0000000000..878b296050
--- /dev/null
+++ b/tests/unit/batch-page-static.test.ts
@@ -0,0 +1,74 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
+const pagePath = path.join(
+ repoRoot,
+ "src/app/(dashboard)/dashboard/batch/page.tsx"
+);
+const enPath = path.join(repoRoot, "src/i18n/messages/en.json");
+
+function readBatchPage() {
+ return fs.readFileSync(pagePath, "utf8");
+}
+
+function readEnKeys() {
+ return Object.keys(JSON.parse(fs.readFileSync(enPath, "utf8")));
+}
+
+test("batch page stable header uses t() for subtitle", () => {
+ const source = readBatchPage();
+ assert.match(source, /batchHeaderSubtitle/);
+});
+
+test("batch page stable header uses t() for three-step strip", () => {
+ const source = readBatchPage();
+ assert.match(source, /batchStep1/);
+ assert.match(source, /batchStep2/);
+ assert.match(source, /batchStep3/);
+ assert.match(source, /batchStep1Desc/);
+ assert.match(source, /batchStep2Desc/);
+ assert.match(source, /batchStep3Desc/);
+});
+
+test("batch page stable header keeps Create batch CTA using t()", () => {
+ const source = readBatchPage();
+ assert.match(source, /batchListNewButton/);
+});
+
+test("batch page still renders collapsible BatchConceptCard as optional deeper explanation", () => {
+ const source = readBatchPage();
+ assert.match(source, /BatchConceptCard/);
+});
+
+test("batch page stable header does not contain hardcoded English", () => {
+ const source = readBatchPage();
+ assert.doesNotMatch(source, /Run many requests as one job/);
+ assert.doesNotMatch(source, /1 \· Upload JSONL/);
+ assert.doesNotMatch(source, /2 \· Create batch/);
+ assert.doesNotMatch(source, /3 \· Get results/);
+});
+
+test("new i18n keys exist in en.json common namespace", () => {
+ const enKeys = JSON.parse(
+ fs.readFileSync(
+ path.join(repoRoot, "src/i18n/messages/en.json"),
+ "utf8"
+ )
+ ).common || {};
+ const requiredKeys = [
+ "batchHeaderSubtitle",
+ "batchStep1",
+ "batchStep2",
+ "batchStep3",
+ "batchStep1Desc",
+ "batchStep2Desc",
+ "batchStep3Desc",
+ ];
+ for (const key of requiredKeys) {
+ assert.ok(key in enKeys, `Missing i18n key in en.json common namespace: ${key}`);
+ }
+});
\ No newline at end of file