fix(documents): say when an extraction stopped short instead of blaming the copy

The document review now names the two outcomes the extraction can report
since it reads a long document in parts. A read that stopped at the
provider's output limit before a single fact closed shows its own
sentence (try a model that allows longer answers) rather than the
generic "Couldn't read the document". A run that staged facts but had a
part cut or not reached opens the review as before and adds a warning
that some values may be missing, so the list is checked against the
document. Both strings are in every locale bundle.

Refs #1257
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 10:34:37 +02:00
1 parent 5a7bd95c7b
commit 5a8f59e0c0
12 files changed
+47 -7

No files matched your search

+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "Zum Scannen von PDFs wird ein Claude-Vision-Anbieter benötigt.",
"errorFileType": "Dieser Dateityp kann nicht gelesen werden.",
"errorNotIndexed": "Lies das Dokument zuerst aus, dann extrahiere.",
"errorOutputLimit": "Der KI-Anbieter hat sein Ausgabelimit erreicht, bevor er einen einzigen Wert geliefert hat. Versuche ein Modell, das längere Antworten erlaubt.",
"errorLocalOcrDisabled": "Aktiviere zuerst die lokale Texterkennung in den KI-Einstellungen.",
"errorProvider": "Dafür ist kein KI-Anbieter eingerichtet.",
"errorTextImageOnly": "Lokale Texterkennung liest nur Bilder, nicht diese Datei.",
@@ -11839,6 +11840,7 @@
"extracting": "Liest aus…",
"extractHint": "Nutzt den bereits ausgelesenen Text dieses Dokuments. Ausgelesene Werte warten auf deine Prüfung; nichts wird automatisch gespeichert.",
"extractEmpty": "Aus diesem Dokument konnten keine Werte ausgelesen werden.",
"extractIncomplete": "Ein Teil des Dokuments konnte nicht vollständig gelesen werden, deshalb können Werte fehlen. Vergleiche die Liste mit dem Dokument.",
"failUnitRequired": "Ein Zahlenwert braucht eine Einheit, bevor er gespeichert werden kann.",
"failUnitMismatch": "Die angegebene Einheit passt nicht zur gespeicherten Einheit dieses Markers. Passe sie an und schließe die Prüfung erneut ab.",
"failGeneric": "Dieser Wert konnte nicht gespeichert werden.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "Scanning PDFs needs a Claude vision provider.",
"errorFileType": "This file type can't be read.",
"errorNotIndexed": "Read the document first, then extract.",
"errorOutputLimit": "The AI provider reached its output limit before it returned a single value. Try a model that allows longer answers.",
"errorLocalOcrDisabled": "Turn on local OCR in the AI settings first.",
"errorProvider": "No AI provider is set up for this.",
"errorTextImageOnly": "Local OCR reads images only, not this file.",
@@ -11839,6 +11840,7 @@
"extracting": "Extracting…",
"extractHint": "Uses the text already read from this document. Extracted values wait for your review; nothing is saved automatically.",
"extractEmpty": "No values could be extracted from this document.",
"extractIncomplete": "Part of the document could not be read in full, so some values may be missing. Check the list against the document.",
"failUnitRequired": "A numeric value needs a unit before it can be saved.",
"failUnitMismatch": "The stated unit does not match the saved unit for this marker. Adjust it, then finish the review again.",
"failGeneric": "This value could not be saved.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "Escanear PDF requiere un proveedor de visión Claude.",
"errorFileType": "Este tipo de archivo no se puede leer.",
"errorNotIndexed": "Primero lee el documento y luego extrae.",
"errorOutputLimit": "El proveedor de IA alcanzó su límite de salida antes de devolver un solo valor. Prueba con un modelo que permita respuestas más largas.",
"errorLocalOcrDisabled": "Activa primero el OCR local en los ajustes de IA.",
"errorProvider": "No hay ningún proveedor de IA configurado para esto.",
"errorTextImageOnly": "El OCR local solo lee imágenes, no este archivo.",
@@ -11839,6 +11840,7 @@
"extracting": "Extrayendo…",
"extractHint": "Usa el texto ya leído de este documento. Los valores extraídos esperan tu revisión; nada se guarda automáticamente.",
"extractEmpty": "No se pudieron extraer valores de este documento.",
"extractIncomplete": "Una parte del documento no se pudo leer por completo, así que pueden faltar valores. Compara la lista con el documento.",
"failUnitRequired": "Un valor numérico necesita una unidad antes de poder guardarse.",
"failUnitMismatch": "La unidad indicada no coincide con la unidad guardada de este marcador. Ajústala y termina la revisión de nuevo.",
"failGeneric": "Este valor no se pudo guardar.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "La lecture des PDF nécessite un fournisseur de vision Claude.",
"errorFileType": "Ce type de fichier ne peut pas être lu.",
"errorNotIndexed": "Lisez d’abord le document, puis extrayez.",
"errorOutputLimit": "Le fournisseur d’IA a atteint sa limite de sortie avant de renvoyer la moindre valeur. Essayez un modèle qui accepte des réponses plus longues.",
"errorLocalOcrDisabled": "Activez d'abord l'OCR local dans les paramètres d'IA.",
"errorProvider": "Aucun fournisseur d'IA n'est configuré pour cela.",
"errorTextImageOnly": "L'OCR local ne lit que les images, pas ce fichier.",
@@ -11839,6 +11840,7 @@
"extracting": "Extraction…",
"extractHint": "Utilise le texte déjà lu de ce document. Les valeurs extraites attendent votre vérification ; rien n’est enregistré automatiquement.",
"extractEmpty": "Aucune valeur n’a pu être extraite de ce document.",
"extractIncomplete": "Une partie du document n’a pas pu être lue entièrement, des valeurs peuvent donc manquer. Comparez la liste avec le document.",
"failUnitRequired": "Une valeur numérique a besoin d’une unité avant de pouvoir être enregistrée.",
"failUnitMismatch": "L’unité indiquée ne correspond pas à l’unité enregistrée pour ce marqueur. Ajustez-la, puis terminez à nouveau la vérification.",
"failGeneric": "Cette valeur n’a pas pu être enregistrée.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "La scansione dei PDF richiede un provider di visione Claude.",
"errorFileType": "Questo tipo di file non può essere letto.",
"errorNotIndexed": "Leggi prima il documento, poi estrai.",
"errorOutputLimit": "Il fornitore di IA ha raggiunto il suo limite di output prima di restituire un solo valore. Prova con un modello che consente risposte più lunghe.",
"errorLocalOcrDisabled": "Attiva prima l'OCR locale nelle impostazioni IA.",
"errorProvider": "Nessun provider IA è configurato per questo.",
"errorTextImageOnly": "L'OCR locale legge solo immagini, non questo file.",
@@ -11839,6 +11840,7 @@
"extracting": "Estrazione…",
"extractHint": "Usa il testo già letto da questo documento. I valori estratti attendono la tua revisione; nulla viene salvato automaticamente.",
"extractEmpty": "Non è stato possibile estrarre valori da questo documento.",
"extractIncomplete": "Una parte del documento non è stata letta per intero, quindi possono mancare dei valori. Confronta l'elenco con il documento.",
"failUnitRequired": "Un valore numerico ha bisogno di un’unità prima di poter essere salvato.",
"failUnitMismatch": "L’unità indicata non corrisponde all’unità salvata per questo marcatore. Correggila e concludi di nuovo la revisione.",
"failGeneric": "Questo valore non è stato salvato.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "PDF를 읽으려면 Claude 비전 제공자가 필요해요.",
"errorFileType": "이 파일 형식은 읽을 수 없어요.",
"errorNotIndexed": "문서를 먼저 읽은 뒤에 추출해 주세요.",
"errorOutputLimit": "AI 제공자가 값을 하나도 돌려주기 전에 출력 한도에 도달했어요. 더 긴 답변을 허용하는 모델로 다시 시도해 주세요.",
"errorLocalOcrDisabled": "AI 설정에서 로컬 OCR을 먼저 켜 주세요.",
"errorProvider": "이 작업에 쓸 AI 제공자가 설정돼 있지 않아요.",
"errorTextImageOnly": "로컬 OCR은 이미지만 읽어요. 이 파일은 읽을 수 없어요.",
@@ -11839,6 +11840,7 @@
"extracting": "추출 중…",
"extractHint": "이 문서에서 이미 읽은 텍스트를 사용해요. 추출한 값은 검토를 기다리며, 자동으로 저장되지 않아요.",
"extractEmpty": "이 문서에서 추출할 수 있는 값이 없었어요.",
"extractIncomplete": "문서 일부를 끝까지 읽지 못해서 빠진 값이 있을 수 있어요. 목록을 문서와 비교해 주세요.",
"failUnitRequired": "숫자 값을 저장하려면 단위가 필요해요.",
"failUnitMismatch": "기재된 단위가 이 지표에 저장된 단위와 달라요. 수정한 뒤 검토를 다시 완료해 주세요.",
"failGeneric": "이 값을 저장하지 못했어요.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "Voor het scannen van PDF's is een Claude-provider met beeldherkenning nodig.",
"errorFileType": "Dit bestandstype kan niet worden gelezen.",
"errorNotIndexed": "Laat het document eerst lezen en extraheer daarna.",
"errorOutputLimit": "De AI-provider bereikte zijn uitvoerlimiet voordat hij één waarde teruggaf. Probeer een model dat langere antwoorden toestaat.",
"errorLocalOcrDisabled": "Zet eerst lokale OCR aan in de AI-instellingen.",
"errorProvider": "Hiervoor is geen AI-provider ingesteld.",
"errorTextImageOnly": "Lokale OCR leest alleen afbeeldingen, niet dit bestand.",
@@ -11839,6 +11840,7 @@
"extracting": "Extraheren…",
"extractHint": "Gebruikt de tekst die al uit dit document is gelezen. Geëxtraheerde waarden wachten op jouw controle; er wordt niets automatisch opgeslagen.",
"extractEmpty": "Er konden geen waarden uit dit document worden geëxtraheerd.",
"extractIncomplete": "Een deel van het document kon niet volledig worden gelezen, dus er kunnen waarden ontbreken. Vergelijk de lijst met het document.",
"failUnitRequired": "Een numerieke waarde heeft een eenheid nodig voordat ze kan worden opgeslagen.",
"failUnitMismatch": "De opgegeven eenheid komt niet overeen met de opgeslagen eenheid voor deze biomarker. Pas de eenheid aan en rond de controle daarna opnieuw af.",
"failGeneric": "Deze waarde kon niet worden opgeslagen.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "Skanowanie plików PDF wymaga dostawcy wizji Claude.",
"errorFileType": "Tego typu pliku nie można odczytać.",
"errorNotIndexed": "Najpierw odczytaj dokument, potem wyodrębnij.",
"errorOutputLimit": "Dostawca AI osiągnął limit odpowiedzi, zanim zwrócił choć jedną wartość. Spróbuj modelu, który pozwala na dłuższe odpowiedzi.",
"errorLocalOcrDisabled": "Najpierw włącz lokalne OCR w ustawieniach AI.",
"errorProvider": "Nie skonfigurowano dostawcy AI do tego celu.",
"errorTextImageOnly": "Lokalne OCR odczytuje tylko obrazy, nie ten plik.",
@@ -11839,6 +11840,7 @@
"extracting": "Odczytywanie…",
"extractHint": "Korzysta z tekstu już odczytanego z tego dokumentu. Odczytane wartości czekają na Twoją weryfikację; nic nie zapisuje się automatycznie.",
"extractEmpty": "Z tego dokumentu nie udało się odczytać żadnych wartości.",
"extractIncomplete": "Części dokumentu nie udało się odczytać w całości, więc może brakować niektórych wartości. Porównaj listę z dokumentem.",
"failUnitRequired": "Wartość liczbowa potrzebuje jednostki, zanim będzie można ją zapisać.",
"failUnitMismatch": "Podana jednostka nie zgadza się z zapisaną jednostką tego wskaźnika. Popraw ją i zakończ przegląd ponownie.",
"failGeneric": "Nie udało się zapisać tej wartości.",
+2
View File
@@ -11779,6 +11779,7 @@
"errorPdfNeedsVision": "Digitalizar PDFs precisa de um provedor de visão Claude.",
"errorFileType": "Este tipo de arquivo não pode ser lido.",
"errorNotIndexed": "Leia o documento primeiro, depois extraia.",
"errorOutputLimit": "O provedor de IA atingiu o limite de saída antes de devolver um único valor. Tente um modelo que permita respostas mais longas.",
"errorLocalOcrDisabled": "Ative o OCR local nas configurações de IA primeiro.",
"errorProvider": "Nenhum provedor de IA está configurado para isso.",
"errorTextImageOnly": "O OCR local lê apenas imagens, não este arquivo.",
@@ -11839,6 +11840,7 @@
"extracting": "Extraindo…",
"extractHint": "Usa o texto já lido deste documento. Valores extraídos aguardam sua revisão; nada é salvo automaticamente.",
"extractEmpty": "Nenhum valor pôde ser extraído deste documento.",
"extractIncomplete": "Parte do documento não pôde ser lida por completo, então podem faltar valores. Confira a lista com o documento.",
"failUnitRequired": "Um valor numérico precisa de uma unidade antes de poder ser salvo.",
"failUnitMismatch": "A unidade informada não corresponde à unidade salva para este marcador. Ajuste-a e conclua a revisão novamente.",
"failGeneric": "Este valor não pôde ser salvo.",
@@ -40,4 +40,10 @@ describe("documentAiErrorKey", () => {
"documents.assist.errorGeneric",
);
});
it("says the output limit stopped the read, not that the copy was unreadable", () => {
expect(
documentAiErrorKey(refusal("documents.inbound.outputLimit", 422)),
).toBe("documents.assist.errorOutputLimit");
});
});
@@ -40,6 +40,7 @@ import { Label } from "@/components/ui/label";
import { MarkerMatchField } from "@/components/labs/marker-match-field";
import type { BiomarkerListResponse } from "@/components/labs/types";
import { apiGet } from "@/lib/api/api-fetch";
import type { DocumentExtractRunResult } from "@/lib/documents/ai-runs/types";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { cn } from "@/lib/utils";
@@ -473,6 +474,25 @@ function FactRow({
* or nothing. Owner-only: staging and confirming write to the caller's own
* record, so a delegate never sees these controls.
*/
/**
* What the person is told when a stored extract finishes. The staged facts
* arrive with the document refetch the hook starts; the review opens onto
* them. A long document read in parts may have had a part cut at the
* provider's output limit or not reached, and then the review is not the
* whole document.
*/
function announceExtract(
result: DocumentExtractRunResult,
t: (key: string) => string,
openReview: () => void,
): void {
if (result.factsStaged > 0) openReview();
else toast.info(t("documents.review.extractEmpty"));
if (result.incomplete) {
toast.warning(t("documents.review.extractIncomplete"));
}
}
export function DocumentFactsSection({
doc,
canManage,
@@ -680,13 +700,7 @@ export function DocumentFactsSection({
onSuccess: (result) => {
setFailures({});
setIncluded({});
// The staged facts arrive with the document refetch the
// hook starts; the review opens onto them.
if (result.factsStaged > 0) {
setOpen(true);
} else {
toast.info(t("documents.review.extractEmpty"));
}
announceExtract(result, t, () => setOpen(true));
},
onError: (error) =>
showErrorToast(aiErrorText(error), { cause: error }),
@@ -222,6 +222,8 @@ export function documentAiErrorKey(err: unknown): string {
return "documents.assist.errorProvider";
case "documents.inbound.notIndexed":
return "documents.assist.errorNotIndexed";
case "documents.inbound.outputLimit":
return "documents.assist.errorOutputLimit";
case "aiRuns.workerUnavailable":
return "aiRuns.errorWorkerUnavailable";
case "aiRuns.timedOut":