Refactor OnelinerDisplay states

Introduce `Generating` state and clarify `Empty` and `Missing` states.
The `Pending` state now specifically refers to transcription being in
progress.
The `Generating` state covers the period after transcription is complete
but before the LLM has produced the final oneliner state, or when a new
state needs to be generated due to new recordings.
The `Missing` state is collapsed into `Empty` as the UI treats them
identically when no title is expected.
This commit is contained in:
2026-04-20 13:01:11 +02:00
parent d65720c671
commit 6886c20e26
2 changed files with 40 additions and 15 deletions
+39 -14
View File
@@ -33,18 +33,23 @@ use crate::transcribe::recovery as transcribe_recovery;
/// exposes the raw [`OnelinerState`] and lets each client decide how /// exposes the raw [`OnelinerState`] and lets each client decide how
/// to render transit states. /// to render transit states.
enum OnelinerDisplay { enum OnelinerDisplay {
/// LLM produced a usable title.
Ready(String), Ready(String),
/// Stable "no title to expect" — either the LLM returned nothing
/// (no medical content in transcript) or every recording failed so
/// nothing can ever generate a title. UI treats both identically;
/// the reason is uninteresting to the doctor in the list view.
Empty, Empty,
/// LLM call failed with transcripts available.
Error, Error,
/// No state file on disk + at least one non-failed recording is /// Transcription still running — a title cannot exist yet.
/// still awaiting its transcript. The oneliner will appear once
/// transcription finishes.
Pending, Pending,
/// No state file on disk and no pending transcription. Rare in /// Transcripts are done (or about to land) but the LLM hasn't
/// practice — the worker writes a state at the end of every job — /// written a state yet, OR a previous state is obsolete because new
/// but possible right after a server crash between transcript and /// recordings arrived. The worker is generating right now or will
/// oneliner write. /// generate as soon as the last transcript is in. Covers the
Missing, /// reset-just-clicked case and the post-restart recovery window.
Generating,
} }
struct UserCaseView { struct UserCaseView {
@@ -527,12 +532,32 @@ async fn compute_case_view(
.all(|r| r.transcript.is_some()); .all(|r| r.transcript.is_some());
let (state, _) = paths::read_oneliner_state(case_path).await; let (state, _) = paths::read_oneliner_state(case_path).await;
let oneliner = match state { let oneliner = if non_failed_count == 0 {
Some(OnelinerState::Ready { text, .. }) => OnelinerDisplay::Ready(text), // All recordings failed — stable end state, no LLM will ever run.
Some(OnelinerState::Empty { .. }) => OnelinerDisplay::Empty, // Missing state collapses to Empty ("unbenannt"): no title is
Some(OnelinerState::Error { .. }) => OnelinerDisplay::Error, // expected, regardless of why.
None if !all_transcribed => OnelinerDisplay::Pending, match state {
None => OnelinerDisplay::Missing, Some(OnelinerState::Ready { text, .. }) => OnelinerDisplay::Ready(text),
Some(OnelinerState::Error { .. }) => OnelinerDisplay::Error,
Some(OnelinerState::Empty { .. }) | None => OnelinerDisplay::Empty,
}
} else if all_transcribed {
// Transcripts done — stable state, except for the brief window
// between transcript-write and state-write where the worker is
// still producing its result.
match state {
Some(OnelinerState::Ready { text, .. }) => OnelinerDisplay::Ready(text),
Some(OnelinerState::Empty { .. }) => OnelinerDisplay::Empty,
Some(OnelinerState::Error { .. }) => OnelinerDisplay::Error,
None => OnelinerDisplay::Generating,
}
} else {
// Transcription still pending. Any persisted state is obsolete:
// the worker will regenerate once the last transcript lands.
match state {
None => OnelinerDisplay::Pending,
Some(_) => OnelinerDisplay::Generating,
}
}; };
let has_document = any_document_exists(case_path).await; let has_document = any_document_exists(case_path).await;
+1 -1
View File
@@ -86,7 +86,7 @@ try {
<div class="check"><input type="checkbox" name="case_id" value="{{ case.case_id }}"></div> <div class="check"><input type="checkbox" name="case_id" value="{{ case.case_id }}"></div>
<div class="body"> <div class="body">
<a href="/web/cases/{{ case.case_id }}"> <a href="/web/cases/{{ case.case_id }}">
<div class="line1"><span class="time"><time datetime="{{ case.recorded_at_iso }}">{{ case.time_hms_utc }}</time></span>{% match case.oneliner %}{% when OnelinerDisplay::Ready with (t) %} — {{ t }}{% when OnelinerDisplay::Empty %} — <span class="empty">kein medizinischer Inhalt</span>{% when OnelinerDisplay::Error %} — <span class="empty">Oneliner-Fehler</span>{% when OnelinerDisplay::Pending %} — <span class="pending">wird transkribiert</span>{% when OnelinerDisplay::Missing %} — <span class="empty">unbenannt</span>{% endmatch %}</div> <div class="line1"><span class="time"><time datetime="{{ case.recorded_at_iso }}">{{ case.time_hms_utc }}</time></span>{% match case.oneliner %}{% when OnelinerDisplay::Ready with (t) %} — {{ t }}{% when OnelinerDisplay::Empty %} — <span class="empty">unbenannt</span>{% when OnelinerDisplay::Error %} — <span class="empty">Fehler</span>{% when OnelinerDisplay::Pending %} — <span class="pending">transkribiere</span>{% when OnelinerDisplay::Generating %} — <span class="pending">generiere Titel …</span>{% endmatch %}</div>
<div class="line2">{{ case.recordings_count }} Aufnahmen{% if case.has_document %} — <span class="status-badge done">ausgewertet</span>{% else %} — <span class="status-badge open">offen</span>{% endif %}{% if case.analyzing %} <span class="label analyzing">wird analysiert</span>{% endif %}</div> <div class="line2">{{ case.recordings_count }} Aufnahmen{% if case.has_document %} — <span class="status-badge done">ausgewertet</span>{% else %} — <span class="status-badge open">offen</span>{% endif %}{% if case.analyzing %} <span class="label analyzing">wird analysiert</span>{% endif %}</div>
{% if is_admin %}<div class="uuid admin-only">{{ case.case_id }}</div>{% endif %} {% if is_admin %}<div class="uuid admin-only">{{ case.case_id }}</div>{% endif %}
</a> </a>