feat: Add web UI for manual oneliner overrides
Adds a new POST endpoint for web-based oneliner overrides and updates the UI to allow manual editing. The person icon replaces the pencil icon for doctor-authored titles, signifying manual authorship rather than editability.
This commit is contained in:
@@ -69,6 +69,12 @@ struct UserCaseView {
|
||||
/// the server's, e.g. server on Bahamas, doctor in Germany).
|
||||
recorded_at_iso: String,
|
||||
oneliner: OnelinerDisplay,
|
||||
/// True iff the persisted oneliner is `OnelinerState::Manual { .. }`
|
||||
/// AND the displayed text actually shows that manual content (i.e.
|
||||
/// not while we're rendering a Pending/Generating placeholder). The
|
||||
/// template uses this to render a 👤 marker next to the title so the
|
||||
/// doctor can tell at a glance which titles they themselves wrote.
|
||||
oneliner_is_manual: bool,
|
||||
recordings_count: usize,
|
||||
analyzing: bool,
|
||||
has_document: bool,
|
||||
@@ -307,6 +313,9 @@ struct CasePageTemplate {
|
||||
case_id: String,
|
||||
case_id_short: String,
|
||||
oneliner: OnelinerDisplay,
|
||||
/// Mirror of `UserCaseView::oneliner_is_manual`. See that field for
|
||||
/// the suppression-during-progress rationale.
|
||||
oneliner_is_manual: bool,
|
||||
/// RFC3339 UTC timestamp of the most recent recording. `None` iff the
|
||||
/// case has no recordings at all. Drives the datetime header under the
|
||||
/// title; browser JS formats it to "Heute 11:34" / "13.12.2022 11:34".
|
||||
@@ -622,7 +631,7 @@ pub async fn handle_case_page(
|
||||
.map(|md| crate::analyze::render::md_to_html(&md));
|
||||
|
||||
let recordings = scan_recordings(&case_dir).await;
|
||||
let oneliner = compute_oneliner_display(&case_dir, &recordings).await;
|
||||
let (oneliner, oneliner_is_manual) = compute_oneliner_display(&case_dir, &recordings).await;
|
||||
|
||||
let a_busy = pipeline.analyze_busy.0.load(Ordering::Acquire);
|
||||
let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), a_busy).await;
|
||||
@@ -648,6 +657,7 @@ pub async fn handle_case_page(
|
||||
case_id: case_id_str,
|
||||
case_id_short,
|
||||
oneliner,
|
||||
oneliner_is_manual,
|
||||
recorded_at_iso,
|
||||
time_hms_utc,
|
||||
document_html,
|
||||
@@ -884,7 +894,7 @@ async fn compute_case_view(
|
||||
.filter(|r| !r.failed)
|
||||
.all(|r| r.transcript.has_file());
|
||||
|
||||
let oneliner = compute_oneliner_display(case_path, &recordings).await;
|
||||
let (oneliner, oneliner_is_manual) = compute_oneliner_display(case_path, &recordings).await;
|
||||
|
||||
let has_document = any_document_exists(case_path).await;
|
||||
let analyzing = !has_document && worker_busy && any_analysis_input_exists(case_path).await;
|
||||
@@ -913,6 +923,7 @@ async fn compute_case_view(
|
||||
time_hms_utc,
|
||||
recorded_at_iso,
|
||||
oneliner,
|
||||
oneliner_is_manual,
|
||||
recordings_count,
|
||||
analyzing,
|
||||
has_document,
|
||||
@@ -961,14 +972,21 @@ async fn ready_text(case_dir: &Path) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the 5-state UI display for the oneliner. Identical logic to
|
||||
/// what `compute_case_view` uses for the case list — extracted so the
|
||||
/// case page can render the same states instead of a plain "Fall"
|
||||
/// fallback.
|
||||
/// Compute the 5-state UI display for the oneliner plus a flag
|
||||
/// indicating manual authorship. Identical logic to what
|
||||
/// `compute_case_view` uses for the case list — extracted so the case
|
||||
/// page can render the same states instead of a plain "Fall" fallback.
|
||||
///
|
||||
/// Returns `(display, is_manual)`. `is_manual` is `true` iff the
|
||||
/// persisted state is `OnelinerState::Manual { .. }` AND the case is
|
||||
/// in a stable state where that text is actually being shown
|
||||
/// (i.e. not `Pending`/`Generating` — those override the persisted
|
||||
/// content with progress placeholders, so the manual flag would be
|
||||
/// misleading there).
|
||||
async fn compute_oneliner_display(
|
||||
case_dir: &Path,
|
||||
recordings: &[RecordingView],
|
||||
) -> OnelinerDisplay {
|
||||
) -> (OnelinerDisplay, bool) {
|
||||
let non_failed_count = recordings.iter().filter(|r| !r.failed).count();
|
||||
let all_transcribed = non_failed_count > 0
|
||||
&& recordings
|
||||
@@ -976,8 +994,9 @@ async fn compute_oneliner_display(
|
||||
.filter(|r| !r.failed)
|
||||
.all(|r| r.transcript.has_file());
|
||||
let (state, _) = paths::read_oneliner_state(case_dir).await;
|
||||
let is_manual = matches!(state, Some(OnelinerState::Manual { .. }));
|
||||
|
||||
if non_failed_count == 0 {
|
||||
let display = if non_failed_count == 0 {
|
||||
// All recordings failed — stable end state, no LLM will ever run.
|
||||
// Missing state collapses to Empty ("unbenannt"): no title is
|
||||
// expected, regardless of why.
|
||||
@@ -1007,7 +1026,17 @@ async fn compute_oneliner_display(
|
||||
None => OnelinerDisplay::Pending,
|
||||
Some(_) => OnelinerDisplay::Generating,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Suppress the manual marker while we're showing a progress
|
||||
// placeholder ("transkribiere…", "generiere Titel…"): the displayed
|
||||
// string isn't the manual text anyway, so a 👤 next to it would lie.
|
||||
let show_manual = is_manual
|
||||
&& !matches!(
|
||||
display,
|
||||
OnelinerDisplay::Pending | OnelinerDisplay::Generating
|
||||
);
|
||||
(display, show_manual)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1021,6 +1050,7 @@ mod tests {
|
||||
time_hms_utc: String::new(),
|
||||
recorded_at_iso: String::new(),
|
||||
oneliner: OnelinerDisplay::Empty,
|
||||
oneliner_is_manual: false,
|
||||
recordings_count: 1,
|
||||
analyzing: false,
|
||||
has_document: false,
|
||||
|
||||
Reference in New Issue
Block a user