import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useSystemState } from '../../composables/useSystemState.js' import { recognizeOcrFiles } from '../../services/ocr.js' import { runOrchestrator } from '../../services/orchestrator.js' const aiAvatar = '/assets/header.png' const userAvatar = '/assets/person.png' const SOURCE_LABELS = { workbench: '来自个人工作台', topbar: '来自发起报销', detail: '来自智能录入', upload: '来自附件上传', requests: '来自报销列表' } const SCENARIO_LABELS = { expense: '报销', accounts_receivable: '应收', accounts_payable: '应付', knowledge: '知识', unknown: '通用' } const INTENT_LABELS = { query: '查询', explain: '解释', compare: '对比', risk_check: '风险检查', draft: '草稿生成', operate: '动作请求' } const DOCUMENT_TYPE_LABELS = { travel_ticket: '行程单/机票/车票', hotel_invoice: '酒店住宿票据', transport_receipt: '交通出行票据', meal_receipt: '餐饮票据', other: '其他单据' } const EXPENSE_TYPE_LABELS = { travel: '差旅费', hotel: '住宿费', transport: '交通费', meal: '伙食费', meeting: '会务费', entertainment: '业务招待费', other: '其他费用' } const REVIEW_SLOT_CONFIG = { expense_type: { title: '报销类型', hint: '请选择本次费用类型', status: '待确认', icon: 'mdi mdi-shape-outline' }, customer_name: { title: '客户单位名称', hint: '请补充客户单位全称', status: '待补充', icon: 'mdi mdi-domain' }, time_range: { title: '业务发生时间', hint: '请确认费用发生日期', status: '待补充', icon: 'mdi mdi-calendar-month-outline' }, location: { title: '业务地点', hint: '请补充业务发生地点', status: '待补充', icon: 'mdi mdi-map-marker-outline' }, merchant_name: { title: '酒店/商户', hint: '请补充酒店或商户名称', status: '待补充', icon: 'mdi mdi-storefront-outline' }, amount: { title: '报销金额', hint: '请补充本次费用金额', status: '待补充', icon: 'mdi mdi-cash' }, reason: { title: '报销事由', hint: '请补充本次费用背景或用途', status: '待补充', icon: 'mdi mdi-text-box-outline' }, participants: { title: '同行人员信息', hint: '请至少填写 1 名同行人员', status: '待补充', icon: 'mdi mdi-account-group-outline' }, attachments: { title: '票据附件', hint: '请上传发票/收据等票据附件', status: '未上传', icon: 'mdi mdi-paperclip' } } const REVIEW_FALLBACK_GROUP_CODES = ['other', 'travel', 'transport', 'hotel', 'meal', 'entertainment'] const REVIEW_CATEGORY_PRESET_OPTIONS = [ { key: 'travel', label: '差旅费' }, { key: 'transport', label: '交通费' }, { key: 'hotel', label: '住宿费' }, { key: 'meal', label: '餐费' }, { key: 'entertainment', label: '业务招待费' }, { key: 'other_trigger', label: '其他类型', is_other: true } ] const REVIEW_OTHER_CATEGORY_OPTIONS = [ { key: 'meeting', label: '会务费' }, { key: 'office', label: '办公费' }, { key: 'training', label: '培训费' }, { key: 'communication', label: '通讯费' }, { key: 'welfare', label: '福利费' }, { key: 'other', label: '其他费用' } ] const REVIEW_SCENE_OPTIONS = ['请客户吃饭', '出差行程', '住宿报销', '交通出行', '会务活动', '其他场景'] let messageSeed = 0 function nowTime() { return new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }) } function createMessage(role, text, attachments = [], extras = {}) { messageSeed += 1 return { id: `msg-${messageSeed}`, role, text, attachments, time: nowTime(), meta: [], citations: [], suggestedActions: [], draftPayload: null, reviewPayload: null, riskFlags: [], ...extras } } function formatMessageTime(value) { if (!value) { return nowTime() } const parsed = new Date(value) if (Number.isNaN(parsed.getTime())) { return nowTime() } return parsed.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }) } function sanitizeRequest(request) { if (!request || typeof request !== 'object') return null const normalized = { id: String(request.id || '').trim(), reason: String(request.reason || request.title || '').trim(), city: String(request.city || request.location || '').trim(), period: String(request.period || '').trim(), applyTime: String(request.applyTime || request.occurredAt || '').trim(), amount: String(request.amount || '').trim(), node: String(request.node || '').trim(), approval: String(request.approval || '').trim(), travel: String(request.travel || '').trim() } return Object.values(normalized).some(Boolean) ? normalized : null } function resolveStatusLabel(status) { if (status === 'succeeded') return '已完成' if (status === 'blocked') return '已阻断' return '失败' } function resolveStatusTone(status) { if (status === 'succeeded') return 'success' if (status === 'blocked') return 'warning' return 'note' } function buildMessageMeta(payload, fileNames = []) { const items = [] if (payload?.selected_agent) { items.push(`Agent: ${payload.selected_agent}`) } if (payload?.permission_level) { items.push(`权限: ${payload.permission_level}`) } if (payload?.trace_summary?.tool_count) { items.push(`工具: ${payload.trace_summary.tool_count}`) } if (payload?.trace_summary?.degraded) { items.push('已降级') } if (payload?.requires_confirmation) { items.push('待确认') } if (payload?.run_id) { items.push(`Run: ${payload.run_id}`) } if (fileNames.length) { items.push(`附件: ${fileNames.length}`) } return items } function buildStoredMessageMeta(messageJson, attachmentNames = []) { const payload = messageJson?.orchestrator_payload if (payload) { return buildMessageMeta(payload, attachmentNames) } const items = [] if (messageJson?.status) { items.push(`状态: ${messageJson.status}`) } if (attachmentNames.length) { items.push(`附件: ${attachmentNames.length}`) } return items } function normalizeOcrDocuments(payload) { const documents = Array.isArray(payload?.documents) ? payload.documents : [] return documents.slice(0, 5).map((item) => ({ filename: item.filename, summary: item.summary, text: String(item.text || '').slice(0, 240), avg_score: Number(item.avg_score || 0), line_count: Number(item.line_count || 0), warnings: Array.isArray(item.warnings) ? item.warnings : [] })) } function buildOcrSummary(payload) { const parts = normalizeOcrDocuments(payload) .map((item) => `${item.filename}:${item.summary || item.text}`) .filter(Boolean) return parts.join(';') } function inferPreviewKind(file) { const mediaType = String(file?.type || '').toLowerCase() const filename = String(file?.name || '').toLowerCase() if (mediaType.startsWith('image/') || /\.(png|jpg|jpeg|webp|bmp)$/i.test(filename)) { return 'image' } if (mediaType.includes('pdf') || /\.pdf$/i.test(filename)) { return 'pdf' } return 'file' } function buildFilePreviews(files, previewRegistry) { return files.map((file) => { const kind = inferPreviewKind(file) if (kind !== 'image') { return { filename: file.name, kind } } const url = URL.createObjectURL(file) previewRegistry.push(url) return { filename: file.name, kind, url } }) } function resolveDocumentPreview(filePreviews, filename) { if (!Array.isArray(filePreviews)) return null return filePreviews.find((item) => item.filename === filename) ?? null } function buildWelcomeInsight(entrySource, linkedRequest) { return { intent: 'welcome', metricLabel: '当前状态', metricValue: '待识别', title: entrySource === 'detail' && linkedRequest?.id ? `已关联 ${linkedRequest.id}` : '等待识别内容', summary: entrySource === 'detail' && linkedRequest?.id ? '发送消息后会直接结合当前单据上下文识别报销语义,右侧展示已识别内容,主对话区展示待补项和风险提示。' : '请输入费用场景或上传票据,右侧会展示已识别内容,主对话区会提示待补信息和风险注意事项。', agent: null } } function buildInitialInsightFromConversation(conversation) { const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : [] for (let index = rawMessages.length - 1; index >= 0; index -= 1) { const item = rawMessages[index] const messageJson = item?.message_json || item?.messageJson || {} const orchestratorPayload = messageJson?.orchestrator_payload || null if (!orchestratorPayload) continue const attachmentNames = Array.isArray(messageJson?.attachment_names) ? messageJson.attachment_names.filter(Boolean) : [] return buildAgentInsight(orchestratorPayload, attachmentNames, []) } return null } function resolveInitialConversationId(conversation) { return String(conversation?.conversation_id || conversation?.conversationId || '').trim() } function resolveInitialDraftClaimId(conversation) { return String(conversation?.draft_claim_id || conversation?.draftClaimId || '').trim() } function normalizeInitialConversationMessages(conversation) { const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : [] return rawMessages.map((item) => { const messageJson = item?.message_json || item?.messageJson || {} const attachmentNames = Array.isArray(messageJson?.attachment_names) ? messageJson.attachment_names.filter(Boolean) : [] const orchestratorPayload = messageJson?.orchestrator_payload || null const result = orchestratorPayload?.result || {} return createMessage(item.role, item.content, attachmentNames, { id: `restored-${item.id || ++messageSeed}`, time: formatMessageTime(item.created_at || item.createdAt), meta: item.role === 'assistant' ? buildStoredMessageMeta(messageJson, attachmentNames) : [], citations: item.role === 'assistant' && Array.isArray(result?.citations) ? result.citations : [], suggestedActions: item.role === 'assistant' && Array.isArray(result?.suggested_actions) ? result.suggested_actions : [], draftPayload: item.role === 'assistant' ? result?.draft_payload || messageJson?.draft_payload || null : null, reviewPayload: item.role === 'assistant' ? result?.review_payload || null : null, riskFlags: item.role === 'assistant' && Array.isArray(result?.risk_flags) ? result.risk_flags : [] }) }) } function cloneReviewEditFields(fields) { const items = Array.isArray(fields) ? fields : [] return items.map((item) => ({ key: String(item?.key || '').trim(), label: String(item?.label || '').trim(), value: String(item?.value || ''), placeholder: String(item?.placeholder || ''), required: Boolean(item?.required), field_type: String(item?.field_type || item?.fieldType || 'text').trim() || 'text', group: String(item?.group || 'basic').trim() || 'basic' })) } function buildReviewFormValues(fields) { return cloneReviewEditFields(fields).reduce((result, item) => { if (!item.key) { return result } result[item.key] = String(item.value || '').trim() return result }, {}) } function buildReviewCorrectionMessage(fields) { const lines = ['请按以下核对后的报销信息更新当前识别结果:'] for (const item of cloneReviewEditFields(fields)) { if (!item.label || (!item.value && !item.required)) { continue } lines.push(`${item.label}:${String(item.value || '').trim() || '待补充'}`) } return lines.join('\n') } function buildReviewEditFieldMap(fields) { return cloneReviewEditFields(fields).reduce((result, item) => { if (!item.key) return result result[item.key] = item return result }, {}) } function createEmptyInlineReviewState() { return { occurred_date: '', amount: '', scene_label: '', reason_value: '', customer_name: '', attachment_names: '', attachment_count: 0, expense_type: '' } } function buildClientTimeContext() { const now = new Date() const locale = typeof navigator !== 'undefined' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN' return { client_now_iso: now.toISOString(), client_timezone_offset_minutes: now.getTimezoneOffset(), client_locale: locale } } function resolveReviewRecognizedSlotCards(reviewPayload) { return Array.isArray(reviewPayload?.slot_cards) ? reviewPayload.slot_cards.filter((item) => item.status !== 'missing') : [] } function resolveReviewMissingSlotCards(reviewPayload) { return Array.isArray(reviewPayload?.slot_cards) ? reviewPayload.slot_cards.filter((item) => item.status === 'missing') : [] } function resolveReviewRiskBriefs(reviewPayload) { return Array.isArray(reviewPayload?.risk_briefs) ? reviewPayload.risk_briefs : [] } function formatConfidenceLabel(value) { const score = Number(value || 0) if (!score) return '待补充' return `${Math.round(score * 100)}%` } function resolveDocumentTypeLabel(type) { return DOCUMENT_TYPE_LABELS[String(type || '').trim()] || DOCUMENT_TYPE_LABELS.other } function resolveExpenseTypeLabel(type, fallbackLabel = '') { const normalized = String(type || '').trim() return EXPENSE_TYPE_LABELS[normalized] || String(fallbackLabel || '').trim() || EXPENSE_TYPE_LABELS.other } function buildReviewRecognizedLines(reviewPayload) { return resolveReviewRecognizedSlotCards(reviewPayload) .filter((item) => String(item?.value || '').trim()) .map((item) => `${item.label}:${item.value}`) } function buildReviewSlotMap(reviewPayload) { return Object.fromEntries( (Array.isArray(reviewPayload?.slot_cards) ? reviewPayload.slot_cards : []).map((item) => [item.key, item]) ) } function resolveExpenseTypeCode(value) { const normalized = String(value || '').trim() if (!normalized) return 'other' if (EXPENSE_TYPE_LABELS[normalized]) return normalized const matched = Object.entries(EXPENSE_TYPE_LABELS).find(([, label]) => label === normalized) return matched?.[0] || 'other' } function formatAmountDisplay(value) { const normalized = String(value || '').trim() const match = normalized.match(/^(\d+(?:\.\d+)?)元$/) if (!match) return normalized const amount = Number(match[1]) if (!Number.isFinite(amount)) return normalized return Number.isInteger(amount) ? `${amount}元` : `${amount.toFixed(2).replace(/\.?0+$/, '')}元` } function buildReviewHeadline(reviewPayload, draftPayload) { const claimNo = String(draftPayload?.claim_no || '').trim() if (claimNo) { return `已为你创建报销草稿 ${claimNo}` } if (reviewPayload?.can_proceed) { return '已为你整理好本次报销信息' } return '已为你整理报销草稿信息' } function buildReviewSubline(reviewPayload, draftPayload) { const claimNo = String(draftPayload?.claim_no || '').trim() if (claimNo) { return `草稿已保存为 draft,你可以继续补充费用明细、客户单位和票据附件。` } if (reviewPayload?.can_proceed) { return '当前关键信息基本齐全,核对无误后可以继续下一步处理。' } return '当前已识别的信息已经整理完成,你可以继续补充缺失项,或者先保存草稿。' } function buildReviewStateLabel(reviewPayload, draftPayload) { if (draftPayload?.claim_no) return '草稿已创建' if (reviewPayload?.can_proceed) return '可继续处理' return '待补充' } function buildReviewStateTone(reviewPayload, draftPayload) { return reviewPayload?.can_proceed || draftPayload?.claim_no ? 'ready' : 'pending' } function buildReviewAlertLabel(slotKey, expenseTypeLabel = '') { if (slotKey === 'customer_name') { return expenseTypeLabel === '业务招待费' ? '业务招待费需补充客户单位名称' : '缺少客户单位名称' } if (slotKey === 'participants') return '缺少同行人员' if (slotKey === 'attachments') return '票据附件未上传' if (slotKey === 'amount') return '报销金额待确认' if (slotKey === 'time_range') return '业务发生时间待确认' if (slotKey === 'reason') return '事由说明待补充' if (slotKey === 'expense_type') return '报销类型待确认' if (slotKey === 'location') return '业务地点待补充' if (slotKey === 'merchant_name') return '酒店/商户待补充' return '仍有信息待补充' } function buildReviewAlertChips(reviewPayload) { const slotMap = buildReviewSlotMap(reviewPayload) const expenseTypeLabel = String(slotMap.expense_type?.value || '').trim() const chips = [] for (const item of resolveReviewMissingSlotCards(reviewPayload).slice(0, 3)) { chips.push({ key: item.key, label: buildReviewAlertLabel(item.key, expenseTypeLabel), tone: item.key === 'attachments' ? 'danger' : 'warning' }) } if (chips.length < 3) { for (const risk of resolveReviewRiskBriefs(reviewPayload)) { if (chips.some((item) => item.label === risk.title)) continue chips.push({ key: risk.title, label: risk.title, tone: risk.level === 'high' ? 'danger' : 'warning' }) if (chips.length >= 3) break } } if (!chips.length && reviewPayload?.can_proceed) { chips.push({ key: 'ready', label: '当前识别信息已可继续处理', tone: 'success' }) } return chips } function buildReviewTodoItems(reviewPayload) { const missingItems = resolveReviewMissingSlotCards(reviewPayload) if (missingItems.length) { return missingItems.map((item) => { const config = REVIEW_SLOT_CONFIG[item.key] || {} return { key: item.key, icon: config.icon || 'mdi mdi-form-select', title: config.title || item.label, hint: item.hint || config.hint || `请补充${item.label}`, status: config.status || '待补充', tone: item.key === 'attachments' ? 'danger' : 'warning' } }) } return resolveReviewRecognizedSlotCards(reviewPayload) .filter((item) => String(item?.value || '').trim()) .slice(0, 3) .map((item) => { const config = REVIEW_SLOT_CONFIG[item.key] || {} return { key: item.key, icon: config.icon || 'mdi mdi-check-circle-outline', title: config.title || item.label, hint: `已识别:${item.value}`, status: '已识别', tone: 'ready' } }) } function resolveReviewPrimaryAction(reviewPayload) { return ( (Array.isArray(reviewPayload?.confirmation_actions) ? reviewPayload.confirmation_actions : []).find( (item) => item.emphasis === 'primary' || ['save_draft', 'next_step'].includes(String(item?.action_type || '')) ) || null ) } function resolveReviewEditAction(reviewPayload) { return ( (Array.isArray(reviewPayload?.confirmation_actions) ? reviewPayload.confirmation_actions : []).find( (item) => String(item?.action_type || '') === 'edit_review' ) || null ) } function buildReviewPrimaryButtonLabel(reviewPayload, draftPayload) { const action = resolveReviewPrimaryAction(reviewPayload) if (!action) return '确认' if (action.action_type === 'save_draft') { return draftPayload?.claim_no ? '确认并继续完善草稿' : '确认并继续生成草稿' } if (action.action_type === 'next_step') { return '确认并进入下一步' } return action.label || '确认' } function buildReviewIntentText(reviewPayload) { const slotMap = buildReviewSlotMap(reviewPayload) const expenseType = String(slotMap.expense_type?.value || '').trim() if (expenseType) { return `报销一笔${expenseType}` } return '发起一笔报销' } function buildReviewSceneValue(reviewPayload) { const slotMap = buildReviewSlotMap(reviewPayload) const reason = String(slotMap.reason?.value || slotMap.reason?.raw_value || '').trim() const expenseType = String(slotMap.expense_type?.value || '').trim() return summarizeReviewScene(reason, expenseType) } function summarizeReviewScene(reason, expenseType = '') { const normalizedReason = String(reason || '').trim() const normalizedExpenseType = String(expenseType || '').trim() const compactReason = normalizedReason.replace(/\s+/g, '') if (compactReason) { if (/请客户.*吃饭|客户.*吃饭|招待|宴请/.test(compactReason)) return '请客户吃饭' if (/出差|差旅/.test(compactReason)) return '出差行程' if (/酒店|住宿/.test(compactReason)) return '住宿报销' if (/交通|打车|车费|停车/.test(compactReason)) return '交通出行' if (/会务|会议|参会/.test(compactReason)) return '会务活动' return compactReason.length > 12 ? `${compactReason.slice(0, 12)}...` : compactReason } if (normalizedExpenseType === '业务招待费') return '请客户吃饭' if (normalizedExpenseType === '差旅费') return '出差行程' if (normalizedExpenseType === '住宿费') return '住宿报销' if (normalizedExpenseType === '交通费') return '交通出行' if (normalizedExpenseType === '会务费') return '会务活动' if (normalizedExpenseType) return normalizedExpenseType return '待补充' } function buildInlineReviewState(reviewPayload) { const slotMap = buildReviewSlotMap(reviewPayload) const editFieldMap = buildReviewEditFieldMap(reviewPayload?.edit_fields) const attachmentNames = String( editFieldMap.attachment_names?.value || slotMap.attachments?.value || (Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards.map((item) => item.filename).join('、') : '') ).trim() const attachmentCount = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards.length : attachmentNames ? attachmentNames.split('、').filter(Boolean).length : 0 const expenseType = String(editFieldMap.expense_type?.value || slotMap.expense_type?.value || '').trim() const reasonValue = String( editFieldMap.reason?.value || slotMap.reason?.value || slotMap.reason?.raw_value || '' ).trim() return { occurred_date: String( editFieldMap.occurred_date?.value || slotMap.time_range?.normalized_value || slotMap.time_range?.value || '' ).trim(), amount: String( editFieldMap.amount?.value || slotMap.amount?.normalized_value || slotMap.amount?.value || '' ).trim(), scene_label: summarizeReviewScene(reasonValue, expenseType), reason_value: reasonValue, customer_name: String(editFieldMap.customer_name?.value || slotMap.customer_name?.value || '').trim(), attachment_names: attachmentNames, attachment_count: attachmentCount, expense_type: expenseType } } function buildReviewAttachmentStatus(reviewPayload) { const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : [] if (!documents.length) return '未上传' return documents.length === 1 ? '已上传 1 份' : `已上传 ${documents.length} 份` } function buildReviewFactCards(reviewPayload, inlineState = createEmptyInlineReviewState()) { const attachmentStatus = inlineState.attachment_count > 0 ? `待保存 ${inlineState.attachment_count} 份` : buildReviewAttachmentStatus(reviewPayload) return [ { key: 'occurred_date', label: '发生时间', value: String(inlineState.occurred_date || '').trim() || '待补充', icon: 'mdi mdi-calendar-month-outline', editor: 'date' }, { key: 'amount', label: '金额', value: formatAmountDisplay(inlineState.amount) || '待补充', icon: 'mdi mdi-cash', editor: 'text' }, { key: 'scene', label: '场景', value: String(inlineState.scene_label || '').trim() || '待补充', icon: 'mdi mdi-silverware-fork-knife', editor: 'select' }, { key: 'customer_name', label: '关联客户', value: String(inlineState.customer_name || '').trim() || '待补充', icon: 'mdi mdi-domain', editor: 'text' }, { key: 'attachments', label: '票据状态', value: attachmentStatus, icon: 'mdi mdi-file-document-outline', editor: 'upload' } ] } function buildReviewCategoryOptions(selectedLabel = '') { const presetLabels = REVIEW_CATEGORY_PRESET_OPTIONS.filter((item) => !item.is_other).map((item) => item.label) return REVIEW_CATEGORY_PRESET_OPTIONS.map((item, index) => ({ ...item, active: item.is_other ? Boolean(selectedLabel) && !presetLabels.includes(selectedLabel) : item.label === selectedLabel, caption: index === 0 ? '常用' : index < 5 ? '常用' : '更多' })) } function buildReviewPanelConfidence(reviewPayload) { const recognized = resolveReviewRecognizedSlotCards(reviewPayload).filter((item) => ['expense_type', 'time_range', 'amount', 'customer_name', 'attachments'].includes(item.key) ) if (!recognized.length) return '0%' const average = recognized.reduce((sum, item) => sum + Number(item.confidence || 0), 0) / recognized.length return formatConfidenceLabel(average) } function buildReviewRiskScore(reviewPayload) { const missingCount = resolveReviewMissingSlotCards(reviewPayload).length const riskPenalty = resolveReviewRiskBriefs(reviewPayload).reduce((sum, item) => { if (item.level === 'high') return sum + 10 if (item.level === 'warning') return sum + 6 return sum + 3 }, 0) const score = 92 - missingCount * 9 - riskPenalty return Math.max(28, Math.min(98, score)) } function buildMissingRiskLine(slotKey, expenseTypeLabel = '') { if (slotKey === 'customer_name') { return expenseTypeLabel === '业务招待费' ? '业务招待费需补充客户单位名称,以便进行合规校验。' : '当前仍缺少客户单位名称,建议补充后再提交。' } if (slotKey === 'participants') { return '缺少同行人员信息,建议补充至少 1 名。' } if (slotKey === 'attachments') { return '尚未上传票据附件,当前无法完成票据核对。' } if (slotKey === 'amount') { return '报销金额仍待确认,提交前需补齐金额信息。' } if (slotKey === 'time_range') { return '业务发生时间仍待确认,建议补充准确日期。' } if (slotKey === 'reason') { return '报销事由说明仍不完整,建议补充业务背景。' } return '当前仍有识别信息待补充,建议先核对后再处理。' } function buildReviewRiskSummary(reviewPayload) { if (resolveReviewMissingSlotCards(reviewPayload).length) { return '存在一定合规风险,请尽快补充完整信息以降低风险。' } if (resolveReviewRiskBriefs(reviewPayload).length) { return '当前识别结果可继续处理,但提交前仍建议核对以下提醒。' } return '当前未发现明显阻断项,确认无误后可以继续下一步。' } function buildReviewRiskItems(reviewPayload) { const slotMap = buildReviewSlotMap(reviewPayload) const expenseTypeLabel = String(slotMap.expense_type?.value || '').trim() const items = [] for (const slot of resolveReviewMissingSlotCards(reviewPayload)) { items.push(buildMissingRiskLine(slot.key, expenseTypeLabel)) } for (const brief of resolveReviewRiskBriefs(reviewPayload)) { if (items.includes(brief.content)) continue items.push(brief.content) } return items.slice(0, 4) } function normalizeInlineReviewComparableState(state) { const source = state && typeof state === 'object' ? state : {} return { occurred_date: String(source.occurred_date || '').trim(), amount: String(source.amount || '').trim(), scene_label: String(source.scene_label || '').trim(), reason_value: String(source.reason_value || '').trim(), customer_name: String(source.customer_name || '').trim(), attachment_names: String(source.attachment_names || '').trim(), expense_type: String(source.expense_type || '').trim() } } function buildInlineReviewChangedLines(baseState, nextState, pendingFiles = []) { const base = normalizeInlineReviewComparableState(baseState) const next = normalizeInlineReviewComparableState(nextState) const lines = [] if (base.occurred_date !== next.occurred_date) { lines.push(`发生时间 ${next.occurred_date || '待补充'}`) } if (base.amount !== next.amount) { lines.push(`金额 ${formatAmountDisplay(next.amount) || '待补充'}`) } if (base.scene_label !== next.scene_label) { lines.push(`场景 ${next.scene_label || '待补充'}`) } if (base.customer_name !== next.customer_name) { lines.push(`关联客户 ${next.customer_name || '待补充'}`) } if (base.expense_type !== next.expense_type) { lines.push(`报销分类 ${next.expense_type || '待补充'}`) } if (base.attachment_names !== next.attachment_names || pendingFiles.length) { lines.push(`票据 ${next.attachment_names || (pendingFiles.length ? `已选择 ${pendingFiles.length} 份附件` : '待上传')}`) } return lines } function buildInlineReviewUserText(baseState, nextState, pendingFiles = []) { const lines = buildInlineReviewChangedLines(baseState, nextState, pendingFiles) if (!lines.length) { return '我已修改识别信息,请按最新内容更新。' } return `我已修改识别信息:${lines.join(',')}。请按最新内容更新。` } function mergeInlineReviewFields(baseFields, inlineState) { const merged = cloneReviewEditFields(baseFields) const updateMap = { expense_type: inlineState.expense_type, occurred_date: inlineState.occurred_date, amount: inlineState.amount, customer_name: inlineState.customer_name, reason: inlineState.reason_value || inlineState.scene_label, attachment_names: inlineState.attachment_names } for (const item of merged) { if (!(item.key in updateMap)) continue item.value = String(updateMap[item.key] || '').trim() } return merged } function buildReviewRecognitionNotes(reviewPayload) { const recognized = resolveReviewRecognizedSlotCards(reviewPayload) const notes = [] const timeSlot = recognized.find((item) => item.key === 'time_range') const sourceLabels = [...new Set(recognized.map((item) => String(item?.source_label || '').trim()).filter(Boolean))] if (timeSlot?.raw_value && timeSlot.raw_value !== timeSlot.value && timeSlot.value) { notes.push(`时间已按你的本地日期换算:${timeSlot.raw_value} -> ${timeSlot.value}`) } if (sourceLabels.length) { notes.push(`本轮主要依据:${sourceLabels.join('、')}`) } const documentCards = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : [] if (documentCards.length) { notes.push(`已关联 ${documentCards.length} 份附件,逐张识别结果已整理在下方`) } else { notes.push('当前还没有上传票据,这一轮主要依据你的文字描述完成初步识别') } return notes } function buildReviewDocumentSummaries(reviewPayload) { const docs = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : [] return docs.map((item) => { const fields = Array.isArray(item.fields) ? item.fields : [] return { ...item, documentTypeLabel: resolveDocumentTypeLabel(item.document_type), expenseTypeLabel: resolveExpenseTypeLabel(item.suggested_expense_type, item.scene_label), confidenceLabel: formatConfidenceLabel(item.avg_score), lines: fields .filter((field) => String(field?.value || '').trim()) .map((field) => `${field.label}:${field.value}`) } }) } function buildReviewDecisionHint(reviewPayload) { const missingSlots = resolveReviewMissingSlotCards(reviewPayload) const riskBriefs = resolveReviewRiskBriefs(reviewPayload) if (reviewPayload?.can_proceed) { return riskBriefs.length ? `我已经把信息整理好了。你可以直接进入下一步,提交前再看一下下方 ${riskBriefs.length} 条提醒。` : '我已经把信息整理好了。你确认无误后,可以直接进入下一步。' } if (missingSlots.length) { return `我先完成了当前这轮识别,还差 ${missingSlots.length} 项关键信息。你可以继续补充;如果暂时拿不全,也可以先保存草稿。` } return '如果你觉得识别结果有偏差,点“修改识别信息”直接校正,我会按新内容重新识别。' } function buildReviewMissingHint(reviewPayload) { const missingSlots = resolveReviewMissingSlotCards(reviewPayload) if (!missingSlots.length) { return '' } if (reviewPayload?.can_proceed) { return '当前关键信息已经齐全,这里无需再补充。' } return '下面这些字段还需要你再确认或补齐,补完后我就能继续往下处理。' } function buildReviewRiskHint(reviewPayload) { const riskBriefs = resolveReviewRiskBriefs(reviewPayload) if (!riskBriefs.length) { return '' } return '这些是我根据当前场景和历史记录给你的提醒,提交前建议顺手核对一下。' } function buildReviewActionHint(reviewPayload) { if (reviewPayload?.can_proceed) { return '如果识别无误,直接点“下一步”;如果有偏差,先修改识别信息。' } return '如果现在信息还不完整,可以先保存草稿;识别错了就点“修改识别信息”。' } function buildReviewStatusTag(reviewPayload) { const missingCount = resolveReviewMissingSlotCards(reviewPayload).length if (reviewPayload?.can_proceed) { return '可继续处理' } if (missingCount > 0) { return `待补充 ${missingCount} 项` } return '待确认' } function buildErrorInsight(error, fileNames = []) { return { intent: 'agent', metricLabel: '运行状态', metricValue: '失败', title: '智能体调用失败', summary: error?.message || '无法连接后端 Orchestrator。', agent: { runId: '未生成', selectedAgent: 'orchestrator', scenario: '未知', intent: '未知', permissionLevel: 'unknown', routeReason: 'request_failed', requiresConfirmation: false, degraded: false, fileNames, citations: [], suggestedActions: [], draftPayload: null, reviewPayload: null, riskFlags: [], toolCount: 0, failedToolCount: 0, selectedCapabilityCodes: [], filePreviews: [], statusLabel: '失败', statusTone: 'note' } } } function buildAgentInsight(payload, fileNames = [], filePreviews = []) { const trace = payload?.trace_summary || {} const result = payload?.result || {} const statusLabel = resolveStatusLabel(payload?.status) return { intent: 'agent', metricLabel: '运行状态', metricValue: statusLabel, title: result?.draft_payload?.title || `${SCENARIO_LABELS[trace?.scenario] || '通用'}${INTENT_LABELS[trace?.intent] || '处理'}结果`, summary: result?.answer || result?.message || '智能体已完成处理。', agent: { runId: payload?.run_id || '未生成', selectedAgent: payload?.selected_agent || 'orchestrator', scenario: SCENARIO_LABELS[trace?.scenario] || trace?.scenario || '未知', intent: INTENT_LABELS[trace?.intent] || trace?.intent || '未知', permissionLevel: payload?.permission_level || 'unknown', routeReason: payload?.route_reason || 'unknown', requiresConfirmation: Boolean(payload?.requires_confirmation), degraded: Boolean(trace?.degraded), fileNames, citations: Array.isArray(result?.citations) ? result.citations : [], suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [], draftPayload: result?.draft_payload || null, reviewPayload: result?.review_payload || null, riskFlags: Array.isArray(result?.risk_flags) ? result.risk_flags : [], toolCount: Number(trace?.tool_count || 0), failedToolCount: Number(trace?.failed_tool_count || 0), selectedCapabilityCodes: Array.isArray(trace?.selected_capability_codes) ? trace.selected_capability_codes : [], filePreviews, statusLabel, statusTone: resolveStatusTone(payload?.status) } } } export default { name: 'TravelReimbursementCreateView', props: { initialPrompt: { type: String, default: '' }, initialFiles: { type: Array, default: () => [] }, initialConversation: { type: Object, default: null }, entrySource: { type: String, default: 'requests' }, requestContext: { type: Object, default: null } }, emits: ['close'], setup(props, { emit }) { const { currentUser } = useSystemState() const fileInputRef = ref(null) const fileInputMode = ref('composer') const messageListRef = ref(null) const composerDraft = ref('') const attachedFiles = ref([]) const submitting = ref(false) const linkedRequest = computed(() => sanitizeRequest(props.requestContext)) const restoredMessages = normalizeInitialConversationMessages(props.initialConversation) const initialInsight = buildInitialInsightFromConversation(props.initialConversation) const messages = ref( restoredMessages.length ? restoredMessages : [ createMessage( 'assistant', props.entrySource === 'detail' && linkedRequest.value?.id ? `已进入统一对话工作台,当前关联单据 ${linkedRequest.value.id}。请描述费用场景或补充票据。` : '这里是统一对话入口。你发送的内容会直接进入真实 Orchestrator 和 User Agent。' ) ] ) const conversationId = ref(resolveInitialConversationId(props.initialConversation)) const draftClaimId = ref(resolveInitialDraftClaimId(props.initialConversation)) const previewRegistry = [] const currentInsight = ref(initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value)) const reviewCancelDialogOpen = ref(false) const reviewEditDialogOpen = ref(false) const reviewActionBusy = ref(false) const reviewEditFields = ref([]) const reviewActionMessageId = ref('') const reviewInlineForm = ref(createEmptyInlineReviewState()) const reviewInlineBaseForm = ref(createEmptyInlineReviewState()) const reviewInlineBaseFields = ref([]) const reviewInlinePendingFiles = ref([]) const reviewInlineEditorKey = ref('') const reviewOtherCategoryOpen = ref(false) const sourceLabel = computed(() => SOURCE_LABELS[props.entrySource] ?? '来自 AI 工作台') const canSubmit = computed( () => !submitting.value && Boolean(composerDraft.value.trim() || attachedFiles.value.length) ) const showInsightPanel = computed(() => currentInsight.value.intent !== 'welcome') const composerPlaceholder = computed(() => { if (props.entrySource === 'detail' && linkedRequest.value?.id) { return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。` } return '例如:查一下本周报销金额、解释酒店超标风险,或根据附件生成报销草稿。' }) const currentIntentLabel = computed(() => { const labels = { welcome: '等待输入', agent: '真实智能体' } return labels[currentInsight.value.intent] ?? 'AI 处理中' }) const latestReviewMessage = computed(() => [...messages.value].reverse().find((item) => item.role === 'assistant' && item.reviewPayload) ?? null ) const activeReviewPayload = computed( () => currentInsight.value.agent?.reviewPayload || latestReviewMessage.value?.reviewPayload || null ) const activeReviewFilePreviews = computed( () => currentInsight.value.agent?.filePreviews || [] ) const reviewIntentText = computed(() => buildReviewIntentText(activeReviewPayload.value)) const reviewFactCards = computed(() => buildReviewFactCards(activeReviewPayload.value, reviewInlineForm.value)) const reviewCategoryOptions = computed(() => buildReviewCategoryOptions(reviewInlineForm.value.expense_type)) const reviewSelectedOtherCategory = computed(() => { const presetLabels = REVIEW_CATEGORY_PRESET_OPTIONS.filter((item) => !item.is_other).map((item) => item.label) return presetLabels.includes(reviewInlineForm.value.expense_type) ? '' : reviewInlineForm.value.expense_type }) const reviewInlineDirty = computed( () => buildInlineReviewChangedLines( reviewInlineBaseForm.value, reviewInlineForm.value, reviewInlinePendingFiles.value ).length > 0 ) const reviewPanelConfidence = computed(() => buildReviewPanelConfidence(activeReviewPayload.value)) const reviewRiskScore = computed(() => buildReviewRiskScore(activeReviewPayload.value)) const reviewRiskSummary = computed(() => buildReviewRiskSummary(activeReviewPayload.value)) const reviewRiskItems = computed(() => buildReviewRiskItems(activeReviewPayload.value)) const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value)) const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value)) const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value)) const shortcuts = computed(() => [ { label: '快速生成报销草稿', icon: 'mdi mdi-file-document-edit-outline', prompt: props.entrySource === 'detail' && linkedRequest.value?.id ? `请基于当前关联单据 ${linkedRequest.value.id} 快速生成报销草稿` : '帮我快速生成一份报销草稿' } ]) watch( () => activeReviewPayload.value, (payload) => { const nextInlineState = buildInlineReviewState(payload) reviewInlineForm.value = { ...nextInlineState } reviewInlineBaseForm.value = { ...nextInlineState } reviewInlineBaseFields.value = cloneReviewEditFields(payload?.edit_fields) reviewInlinePendingFiles.value = [] reviewInlineEditorKey.value = '' reviewOtherCategoryOpen.value = false }, { immediate: true } ) onMounted(() => { currentInsight.value = initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value) if (props.initialPrompt?.trim() || props.initialFiles.length) { composerDraft.value = props.initialPrompt.trim() attachedFiles.value = Array.from(props.initialFiles) submitComposer() } else { nextTick(scrollToBottom) } }) onBeforeUnmount(() => { for (const url of previewRegistry) { URL.revokeObjectURL(url) } }) function scrollToBottom() { if (!messageListRef.value) return messageListRef.value.scrollTop = messageListRef.value.scrollHeight } function replaceMessage(messageId, nextMessage) { const index = messages.value.findIndex((item) => item.id === messageId) if (index === -1) { messages.value.push(nextMessage) return } messages.value.splice(index, 1, nextMessage) } function triggerFileUpload(mode = 'composer') { if (submitting.value || reviewActionBusy.value) return fileInputMode.value = mode fileInputRef.value?.click() } function handleFilesChange(event) { const files = Array.from(event.target.files ?? []) if (fileInputMode.value === 'inline-review' && activeReviewPayload.value) { reviewInlinePendingFiles.value = files reviewInlineForm.value = { ...reviewInlineForm.value, attachment_names: files.map((file) => file.name).join('、'), attachment_count: files.length } reviewInlineEditorKey.value = '' } else { attachedFiles.value = files } fileInputMode.value = 'composer' if (fileInputRef.value) { fileInputRef.value.value = '' } } function runShortcut(prompt) { composerDraft.value = prompt submitComposer() } function openInlineReviewEditor(key) { if (!activeReviewPayload.value || submitting.value || reviewActionBusy.value) return if (key === 'attachments') { triggerFileUpload('inline-review') return } reviewInlineEditorKey.value = reviewInlineEditorKey.value === key ? '' : key if (key !== 'expense_type') { reviewOtherCategoryOpen.value = false } } function closeInlineReviewEditor() { reviewInlineEditorKey.value = '' reviewOtherCategoryOpen.value = false } function commitInlineReviewEditor() { reviewInlineForm.value = { ...reviewInlineForm.value, occurred_date: String(reviewInlineForm.value.occurred_date || '').trim(), amount: String(reviewInlineForm.value.amount || '').trim(), customer_name: String(reviewInlineForm.value.customer_name || '').trim(), scene_label: String(reviewInlineForm.value.scene_label || '').trim(), reason_value: String(reviewInlineForm.value.reason_value || reviewInlineForm.value.scene_label || '').trim(), expense_type: String(reviewInlineForm.value.expense_type || '').trim() } reviewInlineEditorKey.value = '' } function selectInlineScene(scene) { reviewInlineForm.value = { ...reviewInlineForm.value, scene_label: String(scene || '').trim(), reason_value: String(scene || '').trim() } reviewInlineEditorKey.value = '' } function selectReviewCategory(option) { if (!option) return if (option.is_other) { reviewOtherCategoryOpen.value = !reviewOtherCategoryOpen.value return } reviewInlineForm.value = { ...reviewInlineForm.value, expense_type: option.label } reviewOtherCategoryOpen.value = false } function selectReviewOtherCategory(option) { if (!option) return reviewInlineForm.value = { ...reviewInlineForm.value, expense_type: option.label } reviewOtherCategoryOpen.value = false } function queryDraftByClaimNo(claimNo) { const normalized = String(claimNo || '').trim() if (!normalized || submitting.value || reviewActionBusy.value) return submitComposer({ rawText: `查看报销草稿 ${normalized} 的当前信息`, userText: `查看草稿 ${normalized}` }) } function explainCurrentReviewRisk() { if (!activeReviewPayload.value || submitting.value || reviewActionBusy.value) return submitComposer({ rawText: '请解释一下当前这笔报销的合规风险和待补充项。', userText: '查看全部风险项' }) } async function saveInlineReviewChanges() { if (!activeReviewPayload.value || !reviewInlineDirty.value || reviewActionBusy.value) return reviewActionBusy.value = true try { const fields = mergeInlineReviewFields(reviewInlineBaseFields.value, reviewInlineForm.value) await submitComposer({ rawText: buildReviewCorrectionMessage(fields), userText: buildInlineReviewUserText( reviewInlineBaseForm.value, reviewInlineForm.value, reviewInlinePendingFiles.value ), pendingText: '正在保存修改并刷新右侧核对信息...', files: reviewInlinePendingFiles.value, extraContext: { review_action: 'edit_review', review_form_values: buildReviewFormValues(fields) } }) } finally { reviewActionBusy.value = false } } function buildBackendMessage(rawText, fileNames, ocrSummary = '') { const parts = [] const normalizedText = String(rawText || '').trim() if (normalizedText) { parts.push(normalizedText) } else if (fileNames.length) { parts.push(`我上传了 ${fileNames.length} 份票据,请结合附件名称给出报销建议并尽量生成草稿。`) } if (fileNames.length) { parts.push(`附件名称:${fileNames.join('、')}`) } if (ocrSummary) { parts.push(`OCR摘要:${ocrSummary}`) } if (props.entrySource === 'detail' && linkedRequest.value?.id) { parts.push(`关联单号:${linkedRequest.value.id}`) } return parts.join('\n') } async function submitComposer(options = {}) { const rawText = String(options.rawText ?? composerDraft.value).trim() const files = Array.from(options.files ?? attachedFiles.value) if (!rawText && !files.length) return const fileNames = files.map((file) => file.name) const filePreviews = buildFilePreviews(files, previewRegistry) const userText = String(options.userText || '').trim() || rawText || `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。` const extraContext = options.extraContext && typeof options.extraContext === 'object' ? options.extraContext : {} messages.value.push(createMessage('user', userText, fileNames)) const pendingMessage = createMessage('assistant', options.pendingText || '正在识别并更新右侧核对信息...', [], { meta: ['处理中'] }) messages.value.push(pendingMessage) composerDraft.value = '' attachedFiles.value = [] if (fileInputRef.value) { fileInputRef.value.value = '' } submitting.value = true nextTick(scrollToBottom) try { const user = currentUser.value || {} let ocrPayload = null let ocrSummary = '' let ocrDocuments = [] if (files.length) { try { ocrPayload = await recognizeOcrFiles(files) ocrSummary = buildOcrSummary(ocrPayload) ocrDocuments = normalizeOcrDocuments(ocrPayload) } catch (error) { console.warn('OCR request failed:', error) } } const backendMessage = buildBackendMessage(rawText, fileNames, ocrSummary) const payload = await runOrchestrator({ source: 'user_message', user_id: user.username || user.name || 'anonymous', conversation_id: conversationId.value || null, message: backendMessage, context_json: { role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [], is_admin: Boolean(user.isAdmin), name: user.name || '', role: user.role || '', ...buildClientTimeContext(), entry_source: props.entrySource, attachment_names: fileNames, attachment_count: fileNames.length, draft_claim_id: draftClaimId.value || undefined, ocr_summary: ocrSummary, ocr_documents: ocrDocuments, ...(linkedRequest.value ? { request_context: linkedRequest.value } : {}), ...extraContext } }) conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value draftClaimId.value = String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value replaceMessage( pendingMessage.id, createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], { meta: buildMessageMeta(payload, fileNames), citations: Array.isArray(payload?.result?.citations) ? payload.result.citations : [], suggestedActions: Array.isArray(payload?.result?.suggested_actions) ? payload.result.suggested_actions : [], draftPayload: payload?.result?.draft_payload || null, reviewPayload: payload?.result?.review_payload || null, riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : [] }) ) currentInsight.value = buildAgentInsight(payload, fileNames, filePreviews) } catch (error) { replaceMessage( pendingMessage.id, createMessage( 'assistant', error?.message || '无法连接后端 Orchestrator,请稍后重试。', [], { meta: ['调用失败'] } ) ) currentInsight.value = buildErrorInsight(error, fileNames) } finally { submitting.value = false nextTick(scrollToBottom) } } function openCancelReviewDialog(message) { reviewActionMessageId.value = String(message?.id || '') reviewCancelDialogOpen.value = true } function closeCancelReviewDialog() { if (reviewActionBusy.value) return reviewCancelDialogOpen.value = false reviewActionMessageId.value = '' } function confirmCancelReview() { if (reviewActionBusy.value) return reviewCancelDialogOpen.value = false emit('close') } function openEditReviewDialog(message) { reviewEditFields.value = cloneReviewEditFields(message?.reviewPayload?.edit_fields) reviewActionMessageId.value = String(message?.id || '') reviewEditDialogOpen.value = true } function closeEditReviewDialog() { if (reviewActionBusy.value) return reviewEditDialogOpen.value = false reviewEditFields.value = [] reviewActionMessageId.value = '' } async function applyEditedReview() { if (reviewActionBusy.value) return reviewActionBusy.value = true try { const fields = cloneReviewEditFields(reviewEditFields.value) await submitComposer({ rawText: buildReviewCorrectionMessage(fields), userText: '我已修改识别信息,请按最新内容更新。', pendingText: '正在根据修改内容重新识别...', extraContext: { review_action: 'edit_review', review_form_values: buildReviewFormValues(fields) } }) } finally { reviewActionBusy.value = false } closeEditReviewDialog() } async function handleReviewAction(message, action) { const actionType = String(action?.action_type || '').trim() if (!actionType || reviewActionBusy.value) return if (actionType === 'cancel_review') { openCancelReviewDialog(message) return } if (actionType === 'edit_review') { openEditReviewDialog(message) return } if (!['save_draft', 'next_step'].includes(actionType)) { return } reviewActionBusy.value = true try { const fields = cloneReviewEditFields(message?.reviewPayload?.edit_fields) await submitComposer({ rawText: actionType === 'save_draft' ? '请按当前已识别信息先保存草稿,缺失字段后续再补。' : '我已核对右侧识别结果,请进入下一步。', userText: actionType === 'save_draft' ? '我先按当前信息保存草稿。' : '我确认当前识别结果,继续下一步。', pendingText: actionType === 'save_draft' ? '正在保存当前草稿...' : '正在进入下一步...', extraContext: { review_action: actionType, review_form_values: buildReviewFormValues(fields) } }) } finally { reviewActionBusy.value = false } } return { emit, aiAvatar, userAvatar, fileInputRef, messageListRef, composerDraft, attachedFiles, submitting, messages, currentInsight, linkedRequest, sourceLabel, canSubmit, showInsightPanel, composerPlaceholder, currentIntentLabel, latestReviewMessage, activeReviewPayload, activeReviewFilePreviews, reviewIntentText, reviewFactCards, reviewCategoryOptions, reviewSelectedOtherCategory, reviewInlineDirty, reviewInlineForm, reviewInlineEditorKey, reviewOtherCategoryOpen, reviewInlinePendingFiles, REVIEW_SCENE_OPTIONS, REVIEW_OTHER_CATEGORY_OPTIONS, reviewPanelConfidence, reviewRiskScore, reviewRiskSummary, reviewRiskItems, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewCancelDialogOpen, reviewEditDialogOpen, reviewActionBusy, reviewEditFields, shortcuts, resolveReviewMissingSlotCards, resolveReviewRiskBriefs, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewAlertChips, buildReviewTodoItems, resolveReviewPrimaryAction, resolveReviewEditAction, buildReviewPrimaryButtonLabel, buildReviewDecisionHint, buildReviewMissingHint, buildReviewRiskHint, buildReviewActionHint, buildReviewStatusTag, resolveDocumentPreview, triggerFileUpload, handleFilesChange, runShortcut, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, selectInlineScene, selectReviewCategory, selectReviewOtherCategory, queryDraftByClaimNo, explainCurrentReviewRisk, saveInlineReviewChanges, submitComposer, handleReviewAction, closeCancelReviewDialog, confirmCancelReview, closeEditReviewDialog, applyEditedReview } } }