feat: add durable ASR-to-patient extraction and guarded review
This commit is contained in:
@@ -42,7 +42,8 @@
|
||||
<p v-if="editorNotice" class="editor-notice" role="status">{{ editorNotice }}</p>
|
||||
<div class="evidence">
|
||||
<div class="evidence-title">录音原话</div>
|
||||
<blockquote v-for="(quote, index) in editor.evidence" :key="index"><span>{{ quote.text }}</span><el-button v-if="typeof quote.start_ms === 'number'" link type="primary" @click="emit('seek', quote.start_ms)">播放 {{ timestamp(quote.start_ms) }}</el-button></blockquote>
|
||||
<blockquote v-for="(quote, index) in editor.evidence" :key="index"><span>{{ quote.text }}</span><el-button v-if="evidencePlaybackLabel(quote)" link type="primary" @click="seekEvidence(quote)">{{ evidencePlaybackLabel(quote) }}</el-button></blockquote>
|
||||
<p v-if="editor.evidence.some(quote => quote.position_type === 'segment')" class="muted">回听位置为转写片段范围,不是原话的逐字时间戳;请结合原始录音核对。</p>
|
||||
<p v-if="!editor.evidence.length" class="muted">未提供原话证据,请人工核实。</p>
|
||||
<p v-if="editor.date_text || editor.time_text" class="muted">时间原话:{{ editor.date_text || '未提及日期' }} · {{ editor.time_text || '未提及时间' }}</p>
|
||||
</div>
|
||||
@@ -61,9 +62,9 @@
|
||||
<div v-for="field in itemFields(editor)" :key="field.key" class="field-grid">
|
||||
<div class="field-label">{{ field.label }}<small v-if="field.readonly">只读</small></div>
|
||||
<div class="current-value">{{ fieldText(editor.current_values[field.key], field) }}</div>
|
||||
<div class="suggested-value"><FollowupAudioField :field="field" :model-value="editor.values[field.key]" :readonly="disabled || applied || field.readonly" @update:model-value="value => setValue(field.key, value)" /><el-button v-if="!disabled && !applied" link type="danger" @click="removeField(field.key)">移除</el-button></div>
|
||||
<div class="suggested-value"><FollowupAudioField :field="field" :model-value="editor.values[field.key]" :readonly="disabled || applied || field.readonly" @update:model-value="value => setValue(field.key, value)" /><el-button v-if="!disabled && !applied" link type="danger" :disabled="Object.keys(editor.values).length <= 1" @click="removeField(field.key)">移除</el-button></div>
|
||||
</div>
|
||||
<el-select v-if="!disabled && !applied && availableFields(editor).length" class="add-field" placeholder="补充字段" :model-value="''" @update:model-value="(value: string) => addField(value)"><el-option v-for="field in availableFields(editor)" :key="field.key" :label="field.label" :value="field.key" /></el-select>
|
||||
<p v-if="!disabled && !applied" class="muted">仅可更正或移除本项提议字段,不采纳整项请取消勾选;新增内容请返回原诊单编辑人工补录。</p>
|
||||
<el-checkbox v-if="!disabled && !applied" :model-value="!editor.needs_review" class="review-confirm" @update:model-value="(value: boolean | string | number) => confirmReview(!!value)">我已核对原话、冲突 / 重复及日期时间,确认本项内容</el-checkbox>
|
||||
<p v-if="editorIssue && !disabled && !applied" class="review-error" role="status">{{ editorIssue }}</p>
|
||||
</section>
|
||||
@@ -77,7 +78,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { FollowupField, FollowupFields, FollowupReviewItem, FollowupValue } from '@/api/followupAudio'
|
||||
import FollowupAudioField from './FollowupAudioField.vue'
|
||||
import { cloneReviewItems, fieldText, followupKindLabels, hasEditableReviewFields, reviewCounts, reviewFieldIssue, reviewIssue, reviewPending, reviewTimeValue } from './followupAudioState'
|
||||
import { cloneReviewItems, evidencePlaybackLabel, fieldText, followupKindLabels, hasEditableReviewFields, reviewCounts, reviewFieldIssue, reviewIssue, reviewPending, reviewTimeValue } from './followupAudioState'
|
||||
const props = defineProps<{ items: FollowupReviewItem[]; fields: FollowupFields; disabled: boolean; canDaily: boolean; applied?: boolean; uncertainties?: unknown[]; snapshotItems?: FollowupReviewItem[] }>()
|
||||
const emit = defineEmits<{ (event: 'update:items', items: FollowupReviewItem[]): void; (event: 'seek', milliseconds: number): void }>()
|
||||
type Group = 'all' | 'diagnosis' | 'daily' | 'pending'
|
||||
@@ -104,7 +105,6 @@ function patch(id: string, update: Editable) { emit('update:items', props.items.
|
||||
function itemFields(item: FollowupReviewItem): FollowupField[] { return Object.keys(item.values).map(key => (props.fields[item.kind] || []).find(field => field.key === key) || { key, label: key, type: 'text', readonly: true }) }
|
||||
function itemKindLabel(item: FollowupReviewItem) { return followupKindLabels[item.kind] }
|
||||
function itemTitle(item: FollowupReviewItem) { return itemFields(item).map(field => field.label).join('、') || followupKindLabels[item.kind] }
|
||||
function availableFields(item: FollowupReviewItem) { return (props.fields[item.kind] || []).filter(field => !field.readonly && !Object.prototype.hasOwnProperty.call(item.values, field.key)) }
|
||||
function selectionBlocked(item: FollowupReviewItem) { return props.disabled || !!props.applied || !hasEditableReviewFields(item, props.fields) || (item.kind !== 'diagnosis' && !props.canDaily) || !!reviewFieldIssue({ ...item, selected: true }, props.fields) }
|
||||
function toggleSelection(item: FollowupReviewItem, selected: boolean) {
|
||||
if (props.disabled || props.applied) return
|
||||
@@ -127,18 +127,16 @@ function openReview(id: string) {
|
||||
function cancelReview(done?: () => void) { editor.value = null; editorSource.value = ''; editorNotice.value = ''; done?.() }
|
||||
function patchEditor(update: Editable) {
|
||||
if (!editor.value || props.disabled || props.applied) return
|
||||
// Detail/snapshot items are editable drafts, not the server-owned original proposal.
|
||||
if (update.values && Object.keys(update.values).some(key => !Object.prototype.hasOwnProperty.call(editor.value?.values, key))) return
|
||||
editor.value = { ...editor.value, ...update, selected: false, needs_review: true }
|
||||
}
|
||||
function setValue(key: string, value: FollowupValue) {
|
||||
if (!editor.value || !(props.fields[editor.value.kind] || []).some(field => field.key === key && !field.readonly)) return
|
||||
if (!editor.value || !Object.prototype.hasOwnProperty.call(editor.value.values, key)
|
||||
|| !(props.fields[editor.value.kind] || []).some(field => field.key === key && !field.readonly)) return
|
||||
patchEditor({ values: { ...editor.value.values, [key]: value } })
|
||||
}
|
||||
function addField(key: string) {
|
||||
if (!editor.value) return
|
||||
const field = availableFields(editor.value).find(candidate => candidate.key === key)
|
||||
if (field) setValue(key, field.type === 'multiselect' ? [] : null)
|
||||
}
|
||||
function removeField(key: string) { if (!editor.value) return; const values = { ...editor.value.values }; delete values[key]; patchEditor({ values }) }
|
||||
function removeField(key: string) { if (!editor.value || Object.keys(editor.value.values).length <= 1) return; const values = { ...editor.value.values }; delete values[key]; patchEditor({ values }) }
|
||||
function chooseTarget(id: number) {
|
||||
if (!editor.value || (id && !editorCandidates.value.some(candidate => candidate.id === id && !candidate.delete_time))) return
|
||||
patchEditor({ target_id: id || null })
|
||||
@@ -157,7 +155,9 @@ function candidateLabel(item: FollowupReviewItem, candidate: NonNullable<Followu
|
||||
const preview = (props.fields[item.kind] || []).filter(field => Object.prototype.hasOwnProperty.call(candidate.values, field.key)).slice(0, 2).map(field => `${field.label} ${fieldText(candidate.values[field.key], field)}`).join(',')
|
||||
return `#${candidate.id} · ${candidate.record_time || candidate.time_period || '时间未记录'}${candidate.time_estimated ? '(推定)' : ''}${preview ? ` · ${preview}` : ''}`
|
||||
}
|
||||
function timestamp(ms: number) { const seconds = Math.max(0, Math.floor(ms / 1000)); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}` }
|
||||
function seekEvidence(quote: FollowupReviewItem['evidence'][number]) {
|
||||
if (evidencePlaybackLabel(quote) && typeof quote.start_ms === 'number') emit('seek', quote.start_ms)
|
||||
}
|
||||
function uncertaintyText(value: unknown) { return typeof value === 'string' ? value : JSON.stringify(value) }
|
||||
watch(() => props.items.find(item => item.id === editor.value?.id), fresh => {
|
||||
if (!editor.value) return
|
||||
@@ -211,7 +211,6 @@ defineExpose({ hasUnsavedChanges: () => editorDirty.value })
|
||||
.current-value { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.suggested-value { min-width: 0; display: grid; gap: 4px; }
|
||||
.suggested-value > :deep(.el-button) { justify-self: end; font-size: 12px; }
|
||||
.add-field { width: 100%; }
|
||||
.review-confirm { white-space: normal; height: auto; min-height: 32px; align-items: flex-start; }
|
||||
.review-confirm :deep(.el-checkbox__input) { margin-top: 4px; }
|
||||
.review-confirm :deep(.el-checkbox__label) { white-space: normal; line-height: 1.6; font-size: 13px; }
|
||||
|
||||
Reference in New Issue
Block a user