diff --git a/client/src/components/reservation/ReservationTaskDetailPanel.vue b/client/src/components/reservation/ReservationTaskDetailPanel.vue
index 8edddae..49d3b3b 100644
--- a/client/src/components/reservation/ReservationTaskDetailPanel.vue
+++ b/client/src/components/reservation/ReservationTaskDetailPanel.vue
@@ -88,7 +88,7 @@
{{ t('task.taskCardType') }}
- {{ formatReservationTaskCard(t, detail.task_card_type) }}
+ {{ formatReservationTaskCardLabel(t, detail) }}
{{ detail.task_card_type }}
@@ -191,6 +191,79 @@
{{ formatJson(manualReviewRecord) }}
+
+
+
+
+
- {{ t('task.manualResolution.reasonCode') }}
+ -
+ {{ manualReviewEvidence.reasonCode || '-' }}
+
+
+
+
- {{ t('task.manualReviewMissingFields') }}
+ - {{ formatStringList(manualReviewEvidence.missingFields) }}
+
+
+
- {{ t('task.manualReviewBlockingPoints') }}
+ - {{ formatStringList(manualReviewEvidence.blockingPoints) }}
+
+
+
- {{ t('task.manualReviewConflictingPoints') }}
+ - {{ formatStringList(manualReviewEvidence.conflictingPoints) }}
+
+
+
- {{ t('task.manualReviewSuggestedActions') }}
+ - {{ formatStringList(manualReviewEvidence.suggestedHumanActions) }}
+
+
+
- {{ t('task.manualReviewEvidenceToCheck') }}
+ - {{ formatStringList(manualReviewEvidence.evidenceToCheck) }}
+
+
+
+
{{ t('task.parentIdentityCandidates') }}
+
+
+ {{ candidate.title || t('task.parentIdentityCandidate') }}
+ {{ candidate.summary || '-' }}
+ {{ candidate.sourceSummary || '-' }}
+
+
+
- {{ attribute.key }}
+ - {{ attribute.value }}
+
+
+
+
+
+ {{ t('task.noParentIdentityCandidates') }}
+
+
+
+
isReservationFallbackManualReviewTask(detail.value))
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceMessageOnlyResult.value?.manual_review ?? null)
+const manualReviewEvidence = computed(() => buildManualReviewEvidence(detail.value, manualReviewRecord.value))
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
const isFieldRendererReadOnly = computed(
@@ -659,6 +734,123 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
function formatJson(value: unknown): string {
return JSON.stringify(value ?? {}, null, 2)
}
+
+interface ManualReviewEvidenceCandidate {
+ title: string | null
+ summary: string | null
+ sourceSummary: string | null
+ attributes: Array<{
+ key: string
+ value: string
+ }>
+}
+
+interface ManualReviewEvidence {
+ reasonCode: string | null
+ visibleReason: string | null
+ missingFields: string[]
+ blockingPoints: string[]
+ conflictingPoints: string[]
+ suggestedHumanActions: string[]
+ evidenceToCheck: string[]
+ parentIdentityCandidates: ManualReviewEvidenceCandidate[]
+}
+
+function buildManualReviewEvidence(
+ taskDetail: ReservationTaskDetailResult | null,
+ manualReview: ReservationRecord | null | undefined,
+): ManualReviewEvidence | null {
+ if (!manualReview || normalizeStableCode(readString(manualReview.reason_code)) !== 'TARGET_OBJECT_UNCLEAR') {
+ return null
+ }
+ return {
+ reasonCode: readString(manualReview.reason_code),
+ visibleReason: readString(manualReview.visible_reason),
+ missingFields: readStringArray(manualReview.missing_fields),
+ blockingPoints: readStringArray(manualReview.blocking_points),
+ conflictingPoints: readStringArray(manualReview.conflicting_points),
+ suggestedHumanActions: readStringArray(manualReview.suggested_human_actions),
+ evidenceToCheck: readStringArray(manualReview.evidence_to_check),
+ parentIdentityCandidates: readParentIdentityCandidates(taskDetail, manualReview),
+ }
+}
+
+function readParentIdentityCandidates(
+ taskDetail: ReservationTaskDetailResult | null,
+ manualReview: ReservationRecord,
+): ManualReviewEvidenceCandidate[] {
+ const contextCandidates = [
+ readRecord(taskDetail?.context_used)?.parent_identity_candidates,
+ readRecord(manualReview.context_used)?.parent_identity_candidates,
+ manualReview.parent_identity_candidates,
+ ]
+ const candidateList = contextCandidates.find((item) => Array.isArray(item))
+ if (!Array.isArray(candidateList)) {
+ return []
+ }
+ return candidateList
+ .map((item) => readRecord(item))
+ .filter((item): item is ReservationRecord => item !== null)
+ .map((item) => {
+ const attributes = Object.entries(item)
+ .map(([key, value]) => ({
+ key,
+ value: stringifyCandidateValue(value),
+ }))
+ .filter((attribute) => attribute.value !== '')
+ return {
+ title: readFirstString(item, ['field', 'candidate_field', 'key', 'field_path']),
+ summary: readFirstString(item, ['value', 'raw_value', 'normalized_value', 'candidate_value']),
+ sourceSummary: readFirstString(item, ['evidence_source', 'source', 'evidence', 'origin']),
+ attributes,
+ }
+ })
+}
+
+function readRecord(value: unknown): ReservationRecord | null {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
+ return null
+ }
+ return value as ReservationRecord
+}
+
+function readString(value: unknown): string | null {
+ return typeof value === 'string' && value.trim() ? value : null
+}
+
+function readFirstString(record: ReservationRecord, keys: string[]): string | null {
+ for (const key of keys) {
+ const value = readString(record[key])
+ if (value) {
+ return value
+ }
+ }
+ return null
+}
+
+function readStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) {
+ return []
+ }
+ return value.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
+}
+
+function stringifyCandidateValue(value: unknown): string {
+ if (value === undefined || value === null) {
+ return ''
+ }
+ if (typeof value === 'string') {
+ return value
+ }
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value)
+ }
+ return JSON.stringify(value)
+}
+
+function formatStringList(items: string[]): string {
+ return items.length ? items.join(' / ') : '-'
+}