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!
This commit is contained in:
ignamiranda
2026-08-23 05:45:42 -04:00
committed by GitHub
parent ef8414b022
commit 35a34ac34f
4 changed files with 125 additions and 2 deletions

View File

@@ -246,7 +246,42 @@ export default function BatchPage() {
return (
<div className="flex flex-col gap-6">
{/* Concept card (F3) */}
{/* Stable outcome-oriented header (replaces the collapsible card as primary orientation) */}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-[var(--color-text-main)]">{t("batchConceptTitle")}</h1>
<p className="text-sm text-[var(--color-text-muted)]">{t("batchHeaderSubtitle")}</p>
</div>
{/* Three-step strip */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1.5 rounded-lg bg-[var(--color-surface)] border border-[var(--color-border)] px-3 py-2">
<span className="font-medium text-sm text-[var(--color-text-main)]">{t("batchStep1")}</span>
<span className="text-xs text-[var(--color-text-muted)] hidden sm:inline">{t("batchStep1Desc")}</span>
</div>
<span className="material-symbols-outlined text-[var(--color-text-muted)]">chevron_right</span>
<div className="flex items-center gap-1.5 rounded-lg bg-[var(--color-surface)] border border-[var(--color-border)] px-3 py-2">
<span className="font-medium text-sm text-[var(--color-text-main)]">{t("batchStep2")}</span>
<span className="text-xs text-[var(--color-text-muted)] hidden sm:inline">{t("batchStep2Desc")}</span>
</div>
<span className="material-symbols-outlined text-[var(--color-text-muted)]">chevron_right</span>
<div className="flex items-center gap-1.5 rounded-lg bg-[var(--color-surface)] border border-[var(--color-border)] px-3 py-2">
<span className="font-medium text-sm text-[var(--color-text-main)]">{t("batchStep3")}</span>
<span className="text-xs text-[var(--color-text-muted)] hidden sm:inline">{t("batchStep3Desc")}</span>
</div>
</div>
{/* Primary CTA */}
<button
onClick={() => setShowWizard(true)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-[var(--color-accent)] text-white hover:opacity-90 transition-all duration-200 w-fit"
>
<span className="material-symbols-outlined text-[16px]">add</span>
{t("batchListNewButton")}
</button>
</div>
{/* Deeper explanation (optional, collapsible) */}
<BatchConceptCard />
{/* "Batch created" success banner (A-6) — auto-dismiss 5s */}

View File

@@ -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",

View File

@@ -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í.",

View File

@@ -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}`);
}
});