feature: Optional OCR-Assisted Draft Pre-Fill

This commit is contained in:
voltsrage
2026-06-28 01:28:54 +08:00
parent 5039dbb979
commit c160c5f912
42 changed files with 3592 additions and 33 deletions
@@ -79,6 +79,7 @@ export function emptyDraft(
status: 'IN_ENTRY',
batchType,
fieldRequirements: fieldRequirementsForBatchType(batchType),
ocrConfidence: null,
patient: null,
encounter: null,
observations: [],
+12
View File
@@ -52,4 +52,16 @@
.status-badge {
@apply px-2 py-1 rounded-full text-xs font-medium;
}
.ocr-banner {
@apply bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-800;
}
.ocr-high {
@apply border-l-[3px] border-l-green-500;
}
.ocr-medium {
@apply border-l-[3px] border-l-yellow-500;
}
.ocr-low {
@apply border-l-[3px] border-l-red-500;
}
}
@@ -10,6 +10,11 @@
</span>
</div>
<div v-if="ocrConfidence" class="ocr-banner">
Pre-filled by OCR ({{ ocrConfidence.provider }}).
Review all values against the scan before submitting.
</div>
<!-- Patient section -->
<fieldset class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Patient Demographics</legend>
@@ -20,7 +25,7 @@
v-model="patient.fullName"
@blur="savePatient"
type="text"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('patient.fullName')]"
/>
</div>
<div>
@@ -29,12 +34,16 @@
v-model="patient.dateOfBirth"
@blur="savePatient"
type="date"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('patient.dateOfBirth')]"
/>
</div>
<div>
<label class="block text-xs text-gray-500">Sex</label>
<select v-model="patient.sex" @change="savePatient" class="form-input text-sm">
<select
v-model="patient.sex"
@change="savePatient"
:class="['form-input', 'text-sm', fieldConfidenceClass('patient.sex')]"
>
<option value="">Select...</option>
<option value="male">Male</option>
<option value="female">Female</option>
@@ -146,12 +155,16 @@
v-model="encounter.admissionDate"
@blur="saveEncounter"
type="datetime-local"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.admissionDate')]"
/>
</div>
<div>
<label class="block text-xs text-gray-500">Department</label>
<select v-model="encounter.department" @change="saveEncounter" class="form-input text-sm">
<select
v-model="encounter.department"
@change="saveEncounter"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.department')]"
>
<option value=""></option>
<option v-for="dept in departments" :key="dept" :value="dept">{{ dept }}</option>
</select>
@@ -162,7 +175,7 @@
v-model="encounter.roomBed"
@blur="saveEncounter"
type="text"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.roomBed')]"
/>
</div>
<div>
@@ -171,7 +184,7 @@
v-model="encounter.admissionReason"
@blur="saveEncounter"
type="text"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.admissionReason')]"
/>
</div>
<div v-if="showEncounterSummary">
@@ -203,6 +216,7 @@
v-for="obs in observations"
:key="obs.id"
:observation="obs"
:value-input-class="observationValueConfidenceClass(obs.observationCode)"
@update="(field, value) => handleObsUpdate(obs.id, field, value)"
@delete="handleObsDelete"
/>
@@ -233,6 +247,7 @@
import { ref, reactive, computed, watch } from 'vue'
import { useBatchStore } from '../stores/batches'
import { useToast } from '../composables/useToast'
import { useOcrFieldConfidence } from '../composables/useOcrFieldConfidence'
import ObservationRow from '../components/ObservationRow.vue'
import type { BatchDetailResponse, DraftObservation } from '../types'
@@ -271,6 +286,14 @@ const departments = [
]
const fieldReqs = computed(() => batchStore.currentDraft?.fieldRequirements)
const ocrConfidence = computed(() => batchStore.currentDraft?.ocrConfidence ?? null)
const { fieldConfidenceClass } = useOcrFieldConfidence(ocrConfidence)
function observationValueConfidenceClass(observationCode: string): string {
if (!observationCode) return ''
return fieldConfidenceClass(`observation.${observationCode}.value`)
}
const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false)
const showMedications = computed(() => fieldReqs.value?.showMedications ?? false)
const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false)
@@ -29,7 +29,7 @@
@change="update('value', parseFloat(($event.target as HTMLInputElement).value))"
type="number"
step="0.01"
class="form-input text-sm"
:class="['form-input', 'text-sm', valueInputClass]"
:disabled="readonly"
/>
</div>
@@ -96,6 +96,7 @@ const props = defineProps<{
readonly?: boolean
showVerified?: boolean
verified?: boolean
valueInputClass?: string
}>()
const emit = defineEmits<{
@@ -12,6 +12,11 @@
<p class="text-sm text-red-700">{{ batch.rejectionReason }}</p>
</div>
<div v-if="ocrConfidence" class="ocr-banner">
Values pre-filled by OCR ({{ ocrConfidence.provider }}).
Colored borders indicate extraction confidence verify each value against the scan.
</div>
<!-- Patient review -->
<fieldset class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Patient Demographics</legend>
@@ -26,7 +31,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -47,7 +55,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -68,7 +79,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -89,7 +103,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -109,6 +126,7 @@
:readonly="true"
:show-verified="true"
:verified="fieldChecks[`observations[${index}].value`] ?? false"
:value-input-class="observationValueConfidenceClass(obs.observationCode)"
@verify="(_obsId, passed) => toggleCheck(`observations[${index}].value`, passed)"
/>
</div>
@@ -190,6 +208,7 @@ import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useBatchStore } from '../stores/batches'
import { useToast } from '../composables/useToast'
import { useOcrFieldConfidence } from '../composables/useOcrFieldConfidence'
import ObservationRow from '../components/ObservationRow.vue'
import type { BatchDetailResponse, DraftObservation } from '../types'
@@ -221,6 +240,14 @@ const medicationFields = ref<FieldInfo[]>([])
const encounterFields = ref<FieldInfo[]>([])
const fieldReqs = computed(() => batchStore.currentDraft?.fieldRequirements)
const ocrConfidence = computed(() => batchStore.currentDraft?.ocrConfidence ?? null)
const { fieldConfidenceClass } = useOcrFieldConfidence(ocrConfidence)
function observationValueConfidenceClass(observationCode: string): string {
if (!observationCode) return ''
return fieldConfidenceClass(`observation.${observationCode}.value`)
}
const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false)
const showMedications = computed(() => fieldReqs.value?.showMedications ?? false)
const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false)
@@ -0,0 +1,17 @@
import { type ComputedRef } from 'vue'
import type { OcrConfidenceMap } from '../types'
export function useOcrFieldConfidence(
ocrConfidence: ComputedRef<OcrConfidenceMap | null | undefined>,
) {
function fieldConfidenceClass(fieldPath: string): string {
if (!ocrConfidence.value) return ''
const confidence = ocrConfidence.value.fieldConfidences[fieldPath]
if (confidence === undefined) return ''
if (confidence >= 0.85) return 'ocr-high'
if (confidence >= 0.7) return 'ocr-medium'
return 'ocr-low'
}
return { fieldConfidenceClass }
}
+8
View File
@@ -126,11 +126,19 @@ export interface DraftObservation {
note: string | null
}
export interface OcrConfidenceMap {
provider: string
processedAt: string
durationMs: number
fieldConfidences: Record<string, number>
}
export interface BatchDraft {
batchId: string
status: string
batchType: string
fieldRequirements: BatchTypeFieldRequirements
ocrConfidence: OcrConfidenceMap | null
patient: DraftPatient | null
encounter: DraftEncounter | null
observations: DraftObservation[]