feat: clarify training dashboard empty states

This commit is contained in:
2026-05-02 03:34:01 -04:00
parent 35f232bf24
commit 96a67dc5cb
4 changed files with 87 additions and 2 deletions
+36
View File
@@ -22,6 +22,7 @@ const trainingError = document.getElementById('trainingError');
const trainingImportText = document.getElementById('trainingImportText');
const trainingImportBtn = document.getElementById('trainingImportBtn');
const trainingExportBtn = document.getElementById('trainingExportBtn');
const trainingReloadBtn = document.getElementById('trainingReloadBtn');
const trainingAttemptStatus = document.getElementById('trainingAttemptStatus');
const trainingAnnotationText = document.getElementById('trainingAnnotationText');
const trainingSaveAnnotationBtn = document.getElementById('trainingSaveAnnotationBtn');
@@ -425,6 +426,9 @@ function setTrainingError(error) {
}
function markTrainingLoading() {
if (trainingReloadBtn) {
trainingReloadBtn.disabled = true;
}
if (trainingCasesList) {
trainingCasesList.setAttribute('aria-busy', 'true');
setText(trainingCasesList, 'Loading case list. Next step: review available cases or import a case package.');
@@ -433,9 +437,13 @@ function markTrainingLoading() {
trainingProgress.setAttribute('aria-busy', 'true');
setText(trainingProgress, 'Loading progress. Next step: submit an attempt to update progress.');
}
setTrainingAttemptStatus('Training data loading. Next step: wait for cases and progress, then choose a case or import a package.');
}
function markTrainingLoaded() {
if (trainingReloadBtn) {
trainingReloadBtn.disabled = false;
}
if (trainingCasesList) trainingCasesList.setAttribute('aria-busy', 'false');
if (trainingProgress) trainingProgress.setAttribute('aria-busy', 'false');
}
@@ -638,6 +646,29 @@ function renderTrainingProgress(items) {
});
}
function renderTrainingLoadFailure(error) {
if (trainingCasesList) {
clearNode(trainingCasesList);
const empty = document.createElement('div');
empty.className = 'metric training-empty training-state-error';
empty.setAttribute('role', 'listitem');
setText(empty, 'Case list unavailable. Next step: reload training data or import a case package.');
trainingCasesList.appendChild(empty);
}
if (trainingProgress) {
clearNode(trainingProgress);
const empty = document.createElement('div');
empty.className = 'metric training-empty training-state-error';
empty.setAttribute('role', 'listitem');
setText(empty, 'Progress unavailable. Next step: reload training data, then submit an attempt to update progress.');
trainingProgress.appendChild(empty);
}
renderTrainingCaseDetail(null);
setSelectedTrainingCase(null);
setTrainingAttemptControls(false);
setTrainingAttemptStatus('Training data request failed. Next step: reload training data or import a case package.');
}
async function loadTrainingDashboard() {
if (!trainingCasesList || !trainingProgress) return;
@@ -651,7 +682,9 @@ async function loadTrainingDashboard() {
renderTrainingCases(cases.items || []);
renderTrainingProgress(progress.items || []);
setTrainingAttemptStatus('Training data loaded. Next step: choose View detail or Start attempt, or import a case package if the list is empty.');
} catch (error) {
renderTrainingLoadFailure(error);
setTrainingError(error);
} finally {
markTrainingLoaded();
@@ -889,6 +922,9 @@ if (trainingImportBtn) {
if (trainingExportBtn) {
trainingExportBtn.addEventListener('click', exportTrainingCases);
}
if (trainingReloadBtn) {
trainingReloadBtn.addEventListener('click', loadTrainingDashboard);
}
configureAutoRefresh();
loadTrainingDashboard();
refreshDashboard();
+3 -1
View File
@@ -70,7 +70,8 @@
<li><span>Progress</span> Review attempt progress after submission.</li>
</ol>
</div>
<div id="trainingError" class="training-error" role="alert" aria-live="assertive" hidden></div>
<p id="trainingErrorHelp" class="training-help training-next-step">If a public training request fails, use Reload training data or Import cases, then choose a case.</p>
<div id="trainingError" class="training-error" role="alert" aria-live="assertive" aria-describedby="trainingErrorHelp" hidden></div>
<div id="trainingTransferPanel" class="training-transfer" role="region" aria-labelledby="trainingTransferHeading" aria-describedby="trainingWorkflowHelp trainingImportHelp">
<h3 id="trainingTransferHeading">Import / Export</h3>
<p id="trainingImportHelp" class="training-help training-next-step">Paste a case package JSON from a trusted export. Next step: import or export cases before choosing a case.</p>
@@ -81,6 +82,7 @@
</div>
<button id="trainingImportBtn" type="button">Import cases</button>
<button id="trainingExportBtn" type="button">Export cases</button>
<button id="trainingReloadBtn" type="button" aria-controls="trainingCasesList trainingProgress trainingAttemptStatus" aria-describedby="trainingWorkflowHelp trainingErrorHelp">Reload training data</button>
</div>
</div>
<div class="training-grid">
+5 -1
View File
@@ -246,7 +246,7 @@ textarea:focus-visible {
}
.training-toolbar {
display: grid;
grid-template-columns: minmax(260px, 1fr) auto auto;
grid-template-columns: minmax(260px, 1fr) auto auto auto;
gap: 10px;
align-items: end;
}
@@ -336,12 +336,16 @@ textarea:focus-visible {
padding: 10px 12px;
}
.training-actions button:disabled,
.training-toolbar button:disabled,
.training-selected-action button:disabled,
.training-refresh-progress:disabled,
.training-case-card button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.training-state-error {
color: #fecaca;
}
.training-refresh-progress {
margin-bottom: 10px;
}
+43
View File
@@ -336,6 +336,49 @@ class TrainingDashboardM3Tests(unittest.TestCase):
with self.subTest(expected=expected):
self.assertIn(expected, app_js)
def test_training_request_failure_states_offer_reload_and_panel_guidance(self) -> None:
index_html = _read_web_file("index.html")
app_js = _read_web_file("app.js")
styles_css = _read_web_file("styles.css")
parser = _parse_index_html()
attrs_by_id = {attrs["id"]: attrs for _tag, attrs in parser.elements if attrs.get("id")}
self.assertIn('id="trainingReloadBtn"', index_html)
self.assertEqual(parser.buttons_by_id["trainingReloadBtn"].get("type"), "button")
self.assertEqual(
parser.buttons_by_id["trainingReloadBtn"].get("aria-controls"),
"trainingCasesList trainingProgress trainingAttemptStatus",
)
self.assertEqual(
parser.buttons_by_id["trainingReloadBtn"].get("aria-describedby"),
"trainingWorkflowHelp trainingErrorHelp",
)
self.assertEqual(attrs_by_id["trainingError"].get("aria-describedby"), "trainingErrorHelp")
for expected in (
"If a public training request fails, use Reload training data or Import cases, then choose a case.",
"Reload training data",
"const trainingReloadBtn = document.getElementById('trainingReloadBtn')",
"function renderTrainingLoadFailure(error)",
"renderTrainingLoadFailure(error)",
"Case list unavailable. Next step: reload training data or import a case package.",
"Progress unavailable. Next step: reload training data, then submit an attempt to update progress.",
"Training data request failed. Next step: reload training data or import a case package.",
"Training data loading. Next step: wait for cases and progress, then choose a case or import a package.",
"trainingReloadBtn.disabled = true",
"trainingReloadBtn.disabled = false",
"trainingReloadBtn.addEventListener('click', loadTrainingDashboard)",
):
with self.subTest(expected=expected):
self.assertIn(expected, index_html + app_js)
for expected_css in (
".training-toolbar",
"grid-template-columns: minmax(260px, 1fr) auto auto auto;",
):
with self.subTest(expected_css=expected_css):
self.assertIn(expected_css, styles_css)
def test_training_optional_controls_are_guarded(self) -> None:
app_js = _read_web_file("app.js")