feat: clarify training dashboard empty states
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user