feat: Show LLM failure banner and retry button
When an LLM analysis fails, a `.analysis_failed.json` marker is created. If no document exists yet and the auto-trigger is blocked by this marker, the case page must display a banner. This banner provides the failure reason and a button to retry the analysis, ensuring users are not left in a dead-end state. The `read_failure_marker` function is made public to allow the web layer to access this failure information. The `CasePageTemplate` is updated to include `analysis_failed` data, which conditionally renders the new `.failure-banner` HTML. This change prevents cases from becoming unrecoverable due to transient LLM errors.
This commit is contained in:
@@ -164,6 +164,58 @@
|
||||
background: #f6f6f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.failure-banner {
|
||||
margin: 1em 0;
|
||||
padding: 1em 1.2em;
|
||||
background: #fdecea;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
color: #842029;
|
||||
}
|
||||
.failure-banner strong {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
.failure-banner .failure-time {
|
||||
display: block;
|
||||
margin-top: 0.4em;
|
||||
font-size: 0.9em;
|
||||
color: #6b1f25;
|
||||
}
|
||||
.failure-banner details {
|
||||
margin-top: 0.6em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.failure-banner details summary {
|
||||
cursor: pointer;
|
||||
color: #6b1f25;
|
||||
}
|
||||
.failure-banner pre.failure-reason {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0.4em 0 0 0;
|
||||
padding: 0.6em 0.8em;
|
||||
background: #fff;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 3px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.85em;
|
||||
color: #4a1014;
|
||||
}
|
||||
.failure-banner form {
|
||||
margin: 0.8em 0 0 0;
|
||||
}
|
||||
.failure-banner button {
|
||||
padding: 0.4em 1em;
|
||||
background: #b02a37;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.failure-banner button:hover {
|
||||
background: #842029;
|
||||
}
|
||||
.recordings-link {
|
||||
display: inline-block;
|
||||
margin: 1em 0;
|
||||
@@ -533,6 +585,33 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% when None %} {% match analysis_failed %} {% when Some with (failure) %}
|
||||
<div class="failure-banner" role="alert">
|
||||
<strong>Analyse fehlgeschlagen.</strong>
|
||||
Bitte erneut versuchen.
|
||||
<span class="failure-time"
|
||||
>Zuletzt versucht:
|
||||
<time datetime="{{ failure.failed_at }}"
|
||||
>{{ failure.failed_at }}</time
|
||||
></span
|
||||
>
|
||||
<details>
|
||||
<summary>Technische Details</summary>
|
||||
<pre class="failure-reason">{{ failure.reason }}</pre>
|
||||
</details>
|
||||
<form
|
||||
method="post"
|
||||
action="/web/cases/{{ case_id }}/analyze"
|
||||
>
|
||||
{% call csrf::field(csrf_token) %}
|
||||
<input
|
||||
type="hidden"
|
||||
name="return_to"
|
||||
value="/web/cases/{{ case_id }}"
|
||||
/>
|
||||
<button type="submit">Erneut analysieren</button>
|
||||
</form>
|
||||
</div>
|
||||
{% when None %} {% if analyzing %}
|
||||
<div class="placeholder">Wird analysiert …</div>
|
||||
{% else if recordings_count == 0 %}
|
||||
@@ -542,7 +621,7 @@
|
||||
{{ recordings_count }} Aufnahme{% if recordings_count != 1 %}n{%
|
||||
endif %}, davon {{ transcribed_count }} transkribiert.
|
||||
</div>
|
||||
{% endif %} {% endmatch %}
|
||||
{% endif %} {% endmatch %} {% endmatch %}
|
||||
|
||||
<div>
|
||||
<a
|
||||
@@ -567,6 +646,20 @@
|
||||
TimeFmt.dateLabel(d) + " - " + TimeFmt.timeLabel(d);
|
||||
})();
|
||||
|
||||
// Same locale treatment for the failure-banner timestamp.
|
||||
// No-JS fallback is the raw RFC3339 UTC string the server
|
||||
// rendered into the <time> element.
|
||||
(() => {
|
||||
const el = document.querySelector(
|
||||
".failure-banner time[datetime]",
|
||||
);
|
||||
if (!el) return;
|
||||
const d = new Date(el.dateTime);
|
||||
if (isNaN(d.getTime())) return;
|
||||
el.textContent =
|
||||
TimeFmt.dateLabel(d) + " " + TimeFmt.timeLabel(d);
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const cb = document.getElementById("admin-view-toggle");
|
||||
if (!cb) return;
|
||||
|
||||
Reference in New Issue
Block a user