import { computed, nextTick, ref, watch } from 'vue' import { useRouter } from 'vue-router' import { ElDialog } from 'element-plus/es/components/dialog/index.mjs' import ConfirmDialog from '../../components/shared/ConfirmDialog.vue' import TravelReimbursementInsightPanel from '../../components/travel/TravelReimbursementInsightPanel.vue' import TravelReimbursementMessageItem from '../../components/travel/TravelReimbursementMessageItem.vue' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { useTravelReimbursementFlow } from './useTravelReimbursementFlow.js' import { useTravelReimbursementComposerTools } from './useTravelReimbursementComposerTools.js' import { useTravelReimbursementAttachments } from './useTravelReimbursementAttachments.js' import { useTravelReimbursementSessionState } from './useTravelReimbursementSessionState.js' import { useTravelReimbursementReviewDrawer } from './useTravelReimbursementReviewDrawer.js' import { useTravelReimbursementSubmitComposer } from './useTravelReimbursementSubmitComposer.js' import { useTravelReimbursementReviewActions } from './useTravelReimbursementReviewActions.js' import { useTravelReimbursementGuidedFlow } from './useTravelReimbursementGuidedFlow.js' import { useStewardPlanFlow } from './useStewardPlanFlow.js' import { useTravelReimbursementMessageActions } from './useTravelReimbursementMessageActions.js' import { useTravelReimbursementCreateViewUi } from './useTravelReimbursementCreateViewUi.js' import { useTravelReimbursementCreateViewLifecycle } from './useTravelReimbursementCreateViewLifecycle.js' import { STEWARD_ASSISTANT_NAME, useTravelReimbursementStewardRuntime } from './useTravelReimbursementStewardRuntime.js' import { useApplicationPreviewEditor } from './useApplicationPreviewEditor.js' import { normalizeOperationFeedbackContext } from '../../composables/useOperationFeedback.js' import { recognizeOcrFiles } from '../../services/ocr.js' import { fetchAgentRunDetail } from '../../services/agentAssets.js' import { deleteConversation, runOrchestrator } from '../../services/orchestrator.js' import { fetchStewardPlan, fetchStewardPlanStream } from '../../services/steward.js' import { renderMarkdown } from '../../utils/markdown.js' import { clearAssistantSessionSnapshot } from '../../utils/assistantSessionSnapshot.js' import { buildLocalExtractionProgressMessages, buildLocalIntentPreview, shouldRequestExpenseIntentConfirmation, shouldRequestExpenseSceneSelection, summarizeSemanticIntentDetail } from '../../utils/reimbursementTextInference.js' import { buildExpenseIntentConfirmationActions, buildExpenseSceneSelectionActions } from '../../utils/expenseAssistantActions.js' import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js' import { buildLocalApplicationPreviewMessage, normalizeApplicationPreview } from '../../utils/expenseApplicationPreview.js' import { calculateTravelReimbursement, createExpenseClaimItem, fetchExpenseClaims, fetchExpenseClaimAttachmentAsset, fetchExpenseClaimDetail, fetchExpenseClaimItemAttachmentMeta, uploadExpenseClaimItemAttachment } from '../../services/reimbursements.js' import { EXPENSE_TYPE_LABELS, REVIEW_SLOT_CONFIG, REVIEW_CATEGORY_PRESET_OPTIONS, REVIEW_OTHER_CATEGORY_OPTIONS, REVIEW_SCENE_OTHER_OPTION, REVIEW_SCENE_OPTIONS, DATE_INPUT_FORMAT, cloneReviewDocumentDrafts, buildReviewDocumentDrafts, normalizeReviewDocumentComparableValue, buildReviewDocumentCorrectionMessage, buildReviewDocumentCorrectionContext, cloneReviewEditFields, buildReviewFormValues, resolveReviewRecognizedSlotCards, resolveReviewMissingSlotCards, resolveReviewExtraMissingLabels, formatConfidenceLabel, resolveDocumentTypeLabel, resolveExpenseTypeLabel, buildReviewRecognizedLines, buildReviewSlotMap, resolveExpenseTypeCode, isValidIsoDateString, parseAmountNumber, normalizeAmountValue, extractAmountInputValue, inferPresetSceneFromReview, summarizeReviewScene, buildInlineReviewState, resolveReviewCategoryConfidenceScore, buildReviewCategoryOptions, buildReviewPanelConfidence, buildLocallySyncedReviewPayload, buildInlineReviewChangedLines, buildLocalReviewSavedMessage, buildReviewSubmitUserText, mergeInlineReviewFields, buildClientTimeContext, formatDraftApplyTime, formatDateInputValue, buildDraftSavedPayload, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewPlainFollowupCopy, resolveReviewFooterActions, resolveReviewSaveDraftAction, buildReviewPrimaryButtonLabel, buildReviewIntentText, buildReviewSceneValue, buildMissingRiskLine, buildReviewRiskSummary as buildReviewRiskSummaryModel, buildLocalReviewCompletionMessage, buildReviewRecognitionNotes, buildReviewDocumentSummaries } from './travelReimbursementReviewModel.js' import { buildReviewCorrectionMessage, buildReviewFactCards, buildReviewFormContextFromPayload, buildReviewMainMessageText, buildReviewRiskItems, canExposeReviewPanelScope, normalizeReviewPanelScope, resolveReviewRiskBriefs } from './travelReimbursementReviewPanelModel.js' import { buildDraftAssociationQueryPayload, buildExpenseQueryHint, buildExpenseQueryWindowLabel, getExpenseQueryActivePage, getExpenseQueryTotalPages, getExpenseQueryVisibleRecords, normalizeExpenseQueryPayload } from './travelReimbursementExpenseQueryModel.js' import { MAX_ATTACHMENTS, MAX_OCR_DOCUMENTS, VISIBLE_ATTACHMENT_CHIPS, buildAgentInsight, buildErrorInsight, ATTACHMENT_ASSOCIATION_CONFIRM_HREF, buildFileIdentity, buildFilePreviews, buildOcrDocumentsFromReviewPayload, buildOcrFilePreviews, buildOcrSummary, buildOcrSummaryFromDocuments, buildReviewFilePreviewsFromReviewPayload, extractReviewAttachmentNames, isTemporaryPreviewUrl, mergeFilePreviews, mergeFilesWithLimit, mergeUploadAttachmentNames, mergeUploadOcrDocuments, normalizeOcrDocuments, resolveAttachmentPreviewKind, resolveDocumentPreview } from './travelReimbursementAttachmentModel.js' import { ASSISTANT_SESSION_MODE_OPTIONS, ASSISTANT_DISPLAY_NAME, FLOW_STEP_FALLBACKS, HOT_KNOWLEDGE_QUESTIONS, INTENT_LABELS, SCENARIO_LABELS, SESSION_TYPE_BUDGET, SESSION_TYPE_APPLICATION, SESSION_TYPE_APPROVAL, SESSION_TYPE_EXPENSE, SESSION_TYPE_KNOWLEDGE, SESSION_TYPE_STEWARD, canUseBudgetAssistantSession, aiAvatar, buildExpenseIntentConfirmationMessage, buildExpenseSceneSelectionMessage, buildMessageMeta, buildWelcomeInsight, createMessage, filterAssistantSessionModes, hasMeaningfulSessionMessages, resolveAssistantSessionMode, resolveKnowledgeRankLabel, resolveKnowledgeRankTone, sanitizeRequest, summarizeSemanticParseDetail, userAvatar } from './travelReimbursementConversationModel.js' import { useTravelReimbursementSuggestedActions } from './useTravelReimbursementSuggestedActions.js' const COMPOSER_TEXTAREA_HEIGHT = 36 const COMPOSER_MAX_ROWS = 5 const REVIEW_DRAWER_MODE_REVIEW = 'review' const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents' const REVIEW_DRAWER_MODE_RISK = 'risk' const REVIEW_DRAWER_MODE_FLOW = 'flow' const REVIEW_NEXT_STEP_HREF = '#review-next-step' const APPLICATION_SUBMIT_HREF = '#application-submit' const REVIEW_RISK_PANEL_HREF_PREFIX = '#review-risk' const REVIEW_QUICK_EDIT_HREF = '#review-quick-edit' const FLOW_STEP_STATUS_PENDING = 'pending' const FLOW_STEP_STATUS_RUNNING = 'running' const FLOW_STEP_STATUS_COMPLETED = 'completed' const FLOW_STEP_STATUS_FAILED = 'failed' export default { name: 'TravelReimbursementCreateView', components: { ElDialog, ConfirmDialog, TravelReimbursementInsightPanel, TravelReimbursementMessageItem }, props: { initialPrompt: { type: String, default: '' }, initialPromptAutoSubmit: { type: Boolean, default: true }, initialApplicationPreview: { type: Object, default: null }, initialFiles: { type: Array, default: () => [] }, initialConversation: { type: Object, default: null }, initialBudgetContext: { type: Object, default: null }, initialSessionType: { type: String, default: '' }, entrySource: { type: String, default: 'requests' }, requestContext: { type: Object, default: null }, invalidatedDraftClaimId: { type: String, default: '' }, reopenToken: { type: Number, default: 0 } }, emits: ['close', 'draft-saved', 'request-updated'], setup(props, { emit }) { const router = useRouter() const { currentUser, refreshCurrentUserFromBackend } = useSystemState() const { toast } = useToast() const fileInputRef = ref(null) const composerTextareaRef = ref(null) const messageListRef = ref(null) const composerDraft = ref('') const submitting = ref(false) const workbenchVisible = ref(false) const closeAfterBusy = ref(false) const linkedRequest = computed(() => sanitizeRequest(props.requestContext)) const hotKnowledgeQuestions = HOT_KNOWLEDGE_QUESTIONS let sessionRuntimeRefs = {} const { activeSessionType, messages, conversationId, draftClaimId, sessionSnapshots, currentInsight, reviewFilePreviews, composerUploadIntent, guidedFlowState, insightPanelCollapsed, sessionSwitchBusy, buildEmptySessionState, resolveCurrentUserId, persistSessionState, applySessionState, switchSessionType } = useTravelReimbursementSessionState({ props, currentUser, linkedRequest, toast, composerDraft, adjustComposerTextareaHeight, scrollToBottom, getSessionRuntimeRefs: () => sessionRuntimeRefs }) const deleteSessionDialogOpen = ref(false) const applicationSubmitConfirmDialog = ref({ open: false, message: null }) const nextStepConfirmDialog = ref({ open: false, message: null, action: null }) const reviewActionBusy = ref(false) const deleteSessionBusy = ref(false) const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW) const { applicationPreviewEditor, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, isApplicationPreviewEditing, isApplicationPreviewDateEditorOpen, openApplicationPreviewEditor, commitApplicationPreviewEditor, commitApplicationPreviewDateEditor, cancelApplicationPreviewEditor, setApplicationPreviewDateMode, canApplyApplicationPreviewDateSelection, handleApplicationPreviewEditorKeydown } = useApplicationPreviewEditor({ persistSessionState, toast, calculateTravelReimbursement, currentUser }) function applyLinkedApplicationPreviewDateSelection(selection) { const editor = applicationPreviewEditor.value if (editor.fieldKey !== 'time' || !editor.messageId) { return false } const targetMessage = messages.value.find((item) => String(item.id || '') === String(editor.messageId || '') ) if (!targetMessage?.applicationPreview) { return false } applicationPreviewEditor.value = { ...editor, dateMode: selection.mode === 'range' ? 'range' : 'single', singleDate: selection.startDate, rangeStartDate: selection.startDate, rangeEndDate: selection.endDate || selection.startDate } return commitApplicationPreviewDateEditor(targetMessage) } const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE) const isApplicationSession = computed(() => activeSessionType.value === SESSION_TYPE_APPLICATION) const isStewardSession = computed(() => activeSessionType.value === SESSION_TYPE_STEWARD) const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value)) const assistantHeaderTitle = computed(() => (isStewardSession.value ? '小财管家' : '个人工作台')) const assistantHeaderDescription = computed(() => isStewardSession.value ? '统一财务任务编排入口' : '个人工作窗,一站式费控解决枢纽' ) const hasStewardInitialAutoSubmitPayload = computed(() => ( isStewardSession.value && props.initialPromptAutoSubmit !== false && ( Boolean(String(props.initialPrompt || '').trim()) || (Array.isArray(props.initialFiles) && props.initialFiles.length > 0) ) )) const showStewardInitialRecognition = computed(() => ( hasStewardInitialAutoSubmitPayload.value && !messages.value.length && (workbenchVisible.value || submitting.value) )) const { flowRunId, flowSteps, activeFlowSteps, visibleFlowSteps, flowRefreshBusy, completedFlowStepCount, flowOverallStatusTone, flowOverallStatusText, flowTotalDurationText, clearFlowSimulationTimers, resetFlowRun, startFlowTick, stopFlowRuntime, startFlowStep, completeFlowStep, failCurrentFlowStep, startSemanticFlowPreview, startExpenseSceneSelectionFlowPreview, startExpenseIntentConfirmationFlowPreview, startExpenseSceneSelectionAfterIntentConfirmation, startReviewActionFlowStep, startExpenseClaimDraftFlowStep, completeFlowResult, refreshFlowRunDetail, formatFlowStepDuration, resolveFlowStepStatusLabel, resolveFlowStepDetail } = useTravelReimbursementFlow({ activeSessionType, reviewDrawerMode, insightPanelCollapsed, isKnowledgeSession, fetchAgentRunDetail, buildLocalIntentPreview, buildLocalExtractionProgressMessages, summarizeSemanticIntentDetail, summarizeSemanticParseDetail, SCENARIO_LABELS, INTENT_LABELS, EXPENSE_TYPE_LABELS, FLOW_STEP_FALLBACKS, REVIEW_DRAWER_MODE_FLOW, REVIEW_DRAWER_MODE_REVIEW, FLOW_STEP_STATUS_PENDING, FLOW_STEP_STATUS_RUNNING, FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED }) const hasScopedReviewPayload = computed(() => { const agent = currentInsight.value.agent || null if (agent?.reviewPayload && canExposeReviewPanelScope(agent.reviewPanelScope)) { return true } if (currentInsight.value.intent === 'agent' && agent) { return false } return messages.value.some((item) => item.role === 'assistant' && item.reviewPayload && canExposeReviewPanelScope(item.reviewPanelScope) ) }) const hasQueryInsight = computed(() => Boolean(currentInsight.value.agent?.queryPayload)) const hasInsightPanelContent = computed(() => { return isKnowledgeSession.value || hasScopedReviewPayload.value || hasQueryInsight.value || activeFlowSteps.value.length > 0 }) const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value) const insightPanelToggleLabel = computed(() => showInsightPanel.value ? '隐藏详细信息' : '展开详细信息' ) const composerPlaceholder = computed(() => { if (isStewardSession.value) { return '例如:申请7月2日去北京出差,同时报销昨天交通费和6月3日上海出差费用。' } if (isKnowledgeSession.value) { return '例如:差旅住宿标准是什么?发票抬头不一致还能报销吗?' } if (props.entrySource === 'detail' && linkedRequest.value?.id) { return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。` } if (activeSessionType.value === SESSION_TYPE_APPLICATION) { return '例如:我想先申请一笔差旅费用,去上海支持项目部署。' } if (activeSessionType.value === SESSION_TYPE_APPROVAL) { return '例如:查一下待我审核的单据,或帮我生成这张单据的审核意见。' } if (activeSessionType.value === SESSION_TYPE_BUDGET) { return '例如:查询市场部 Q1 预算编制情况,重点看差旅、通信、招待费和办公用品。' } return '例如:查一下近10日报销金额、解释酒店超标风险,或根据附件整理报销核对信息。' }) const currentIntentLabel = computed(() => { if (isKnowledgeSession.value && currentInsight.value.intent === 'welcome') { return '热门问题' } const labels = isKnowledgeSession.value ? { welcome: '热门问题', agent: '知识回答' } : { welcome: activeAssistantMode.value?.label || '财务助手', agent: '处理中' } return labels[currentInsight.value.intent] ?? 'AI 处理中' }) const canDeleteCurrentSession = computed( () => Boolean(conversationId.value) || hasMeaningfulSessionMessages(messages.value) ) const latestReviewMessage = computed(() => [...messages.value].reverse().find((item) => item.role === 'assistant' && item.reviewPayload && canExposeReviewPanelScope(item.reviewPanelScope) ) ?? null ) const activeReviewPanelScope = computed(() => { const agent = currentInsight.value.agent || null const agentScope = normalizeReviewPanelScope(agent?.reviewPanelScope) if (agent?.reviewPayload && agentScope) { return agentScope } if (currentInsight.value.intent === 'agent' && agent) { return '' } return normalizeReviewPanelScope(latestReviewMessage.value?.reviewPanelScope) }) const activeReviewPayload = computed(() => { const agent = currentInsight.value.agent || null if (agent?.reviewPayload && normalizeReviewPanelScope(agent.reviewPanelScope)) { return agent.reviewPayload } if (currentInsight.value.intent === 'agent' && agent) { return null } return latestReviewMessage.value?.reviewPayload || null }) const reviewRiskBriefResolver = (payload) => resolveReviewRiskBriefs(payload) const buildReviewRiskSummary = (payload) => buildReviewRiskSummaryModel(payload, reviewRiskBriefResolver) const { reviewInlineForm, reviewInlineBaseForm, reviewInlineBaseFields, reviewInlinePendingFiles, reviewInlineEditorKey, reviewInlineErrors, reviewOtherCategoryOpen, reviewDocumentDrafts, reviewDocumentBaseDrafts, activeReviewDocumentIndex, documentPreviewDialog, activeReviewFilePreviews, reviewIntentText, reviewFactCards, reviewCategoryOptions, reviewOtherCategoryOptions, reviewSelectedOtherCategory, reviewInlineDirty, reviewPanelConfidence, reviewRiskSummary, reviewRiskItems, reviewRiskEmpty, reviewOverviewDrawerAvailable, reviewDocumentDrawerAvailable, reviewRiskDrawerAvailable, reviewFlowDrawerAvailable, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewDocumentCount, isReviewDocumentDrawer, isReviewRiskDrawer, isReviewFlowDrawer, reviewDrawerTitle, reviewDocumentDrawerLabel, reviewDocumentDrawerIcon, reviewRiskDrawerLabel, reviewRiskDrawerIcon, reviewFlowDrawerLabel, reviewFlowDrawerIcon, activeReviewDocument, activeReviewDocumentPreview, canPreviewActiveReviewDocument, reviewDocumentDirty, reviewHasUnsavedChanges, setInlineReviewFieldError, clearInlineReviewFieldError, resetReviewDrawerFromPayload, enforceReviewDrawerAvailability, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, selectInlineScene, selectReviewCategory, selectReviewOtherCategory, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview } = useTravelReimbursementReviewDrawer({ activeReviewPayload, activeReviewPanelScope, reviewFilePreviews, flowSteps: activeFlowSteps, submitting, reviewActionBusy, triggerFileUpload: (...args) => triggerFileUpload(...args), resolveDocumentPreview, buildReviewFactCards, buildReviewRiskItems, buildReviewRiskSummary, buildReviewIntentText, resolveReviewRiskBriefs, reviewDrawerMode, REVIEW_DRAWER_MODE_REVIEW, REVIEW_DRAWER_MODE_DOCUMENTS, REVIEW_DRAWER_MODE_RISK, REVIEW_DRAWER_MODE_FLOW }) const { composerDatePickerOpen, composerDateMode, composerSingleDate, composerRangeStartDate, composerRangeEndDate, composerBusinessTimeTags, composerBusinessTimeDraftTouched, composerCanApplyDateSelection, travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, buildComposerBusinessTimeLabel, hasComposerBusinessTimeSelection, buildComposerBusinessTimeContext, mergeBusinessTimeIntoExtraContext, syncComposerBusinessTimeToReviewCard, resolveComposerSubmitText, resolveComposerDisplaySubmitText, toggleComposerDatePicker, closeComposerDatePicker, setComposerDateMode, handleComposerDateInputChange, removeComposerBusinessTimeTag, handleComposerDatePickerOutside, applyComposerDateSelection, resolveTravelCalculatorInitialDays, resolveTravelCalculatorInitialLocation, openTravelCalculator: openTravelCalculatorInternal, toggleTravelCalculator: toggleTravelCalculatorInternal, closeTravelCalculator, formatTravelCalculatorMoney, buildTravelCalculatorResultText, submitTravelCalculator: submitTravelCalculatorInternal } = useTravelReimbursementComposerTools({ currentUser, activeReviewPayload, reviewInlineForm, latestReviewMessage, currentInsight, messages, composerDraft, composerTextareaRef, adjustComposerTextareaHeight, scrollToBottom, toast, calculateTravelReimbursement, createMessage, buildReviewSlotMap, isValidIsoDateString, buildLocallySyncedReviewPayload, formatDateInputValue, onComposerDateSelection: applyLinkedApplicationPreviewDateSelection }) function syncComposerDateFromApplicationEditor() { const editor = applicationPreviewEditor.value const today = formatDateInputValue() composerDateMode.value = editor.dateMode === 'range' ? 'range' : 'single' composerSingleDate.value = editor.singleDate || today composerRangeStartDate.value = editor.rangeStartDate || composerSingleDate.value || today composerRangeEndDate.value = editor.rangeEndDate || composerRangeStartDate.value || today composerDatePickerOpen.value = true travelCalculatorOpen.value = false } function openApplicationPreviewEditorFromUi(message, fieldKey, value) { openApplicationPreviewEditor(message, fieldKey, value) if (fieldKey === 'time' && isApplicationPreviewEditing(message, 'time')) { syncComposerDateFromApplicationEditor() } } watch(composerDatePickerOpen, (open, previousOpen) => { if (!open && previousOpen && applicationPreviewEditor.value.fieldKey === 'time') { cancelApplicationPreviewEditor() } }) const canShowTravelCalculator = computed(() => activeSessionType.value === SESSION_TYPE_EXPENSE) const { fileInputMode, attachedFiles, composerFilesExpanded, visibleAttachedFiles, hiddenAttachedFileCount, rememberFilePreviews, buildComposerFilePreviews, resolveActiveClaimId, restorePersistedDraftAttachmentPreviews, syncComposerFilesToDraft, triggerFileUpload, handleFilesChange, toggleAttachedFilesExpanded, removeAttachedFile, clearAttachedFiles, stopAttachmentRuntime } = useTravelReimbursementAttachments({ isKnowledgeSession, reviewFilePreviews, linkedRequest, draftClaimId, activeReviewPayload, reviewInlinePendingFiles, reviewInlineForm, reviewInlineEditorKey, composerUploadIntent, submitting, reviewActionBusy, toast, fileInputRef, createExpenseClaimItem, fetchExpenseClaimDetail, fetchExpenseClaimItemAttachmentMeta, fetchExpenseClaimAttachmentAsset, uploadExpenseClaimItemAttachment, extractReviewAttachmentNames, mergeFilesWithLimit, mergeFilePreviews, isTemporaryPreviewUrl, resolveAttachmentPreviewKind, resolveDocumentPreview, buildFilePreviews, buildFileIdentity, MAX_ATTACHMENTS, VISIBLE_ATTACHMENT_CHIPS, clearInlineReviewFieldError }) sessionRuntimeRefs = { attachedFiles, composerFilesExpanded, guidedFlowState } const promptedOperationFeedbackRunIds = new Set() function emitOperationCompleted(payload = {}, extras = {}) { const runId = String(payload?.run_id || payload?.runId || '').trim() const operationStatus = String(payload?.status || '').trim() if (!runId || promptedOperationFeedbackRunIds.has(runId) || operationStatus !== 'succeeded') { return null } const result = payload?.result && typeof payload.result === 'object' ? payload.result : {} promptedOperationFeedbackRunIds.add(runId) return normalizeOperationFeedbackContext({ run_id: runId, conversation_id: String(payload?.conversation_id || payload?.conversationId || conversationId.value || '').trim(), user_id: resolveCurrentUserId(), selected_agent: String(payload?.selected_agent || payload?.selectedAgent || '').trim(), source: 'user_message', session_type: activeSessionType.value, operation_type: String(extras.operationType || 'assistant_round').trim(), operation_status: operationStatus, status: operationStatus, route_reason: String(payload?.route_reason || payload?.routeReason || '').trim(), entry_source: props.entrySource, trace_summary: payload?.trace_summary || payload?.traceSummary || null, result_summary: String(result.answer || result.message || '').trim() }, currentUser.value || {}) } const { confirmPendingAttachmentAssociationInternal, submitComposerInternal } = useTravelReimbursementSubmitComposer({ MAX_ATTACHMENTS, activeReviewPayload, activeSessionType, adjustComposerTextareaHeight, attachedFiles, buildAgentInsight, buildClientTimeContext, buildComposerBusinessTimeContext, buildComposerFilePreviews, buildDraftAssociationQueryPayload, buildErrorInsight, buildExpenseIntentConfirmationActions, buildExpenseIntentConfirmationMessage, buildExpenseSceneSelectionActions, buildExpenseSceneSelectionMessage, buildMessageMeta, buildOcrDocumentsFromReviewPayload, buildOcrFilePreviews, buildOcrSummary, buildOcrSummaryFromDocuments, buildReviewFormContextFromPayload, clearAttachedFiles, clearFlowSimulationTimers, completeFlowResult, completeFlowStep, composerBusinessTimeDraftTouched, composerBusinessTimeTags, composerDraft, composerUploadIntent, conversationId, createMessage, currentInsight, currentUser, refreshCurrentUserFromBackend, draftClaimId, extractReviewAttachmentNames, failCurrentFlowStep, fetchExpenseClaims, fileInputRef, flowRunId, insightPanelCollapsed, isKnowledgeSession, linkedRequest, mergeBusinessTimeIntoExtraContext, mergeFilePreviews, mergeFilesWithLimit, mergeUploadAttachmentNames, mergeUploadOcrDocuments, messages, nextTick, normalizeExpenseQueryPayload, normalizeOcrDocuments, persistSessionState, props, recognizeOcrFiles, refreshFlowRunDetail, rememberFilePreviews, replaceMessage, resolveComposerDisplaySubmitText, resetFlowRun, resolveComposerSubmitText, reviewInlineForm, runOrchestrator, scrollToBottom, sessionSwitchBusy, shouldRequestExpenseIntentConfirmation, shouldRequestExpenseSceneSelection, startExpenseClaimDraftFlowStep, startExpenseIntentConfirmationFlowPreview, startExpenseSceneSelectionFlowPreview, startFlowStep, startSemanticFlowPreview, submitting, syncComposerFilesToDraft, emitOperationCompleted, emitDraftSaved: (payload) => emit('draft-saved', payload), emitRequestUpdated: (payload) => emit('request-updated', payload), toast }) const canSubmit = computed( () => !submitting.value && !sessionSwitchBusy.value && Boolean( composerDraft.value.trim() || attachedFiles.value.length || composerBusinessTimeTags.value.length ) ) const { handleGuidedShortcut, handleGuidedComposerSubmit, handleGuidedSuggestedAction, handleSceneSelectionApplicationGate, resetGuidedFlowState } = useTravelReimbursementGuidedFlow({ guidedFlowState, messages, composerDraft, attachedFiles, composerBusinessTimeTags, composerBusinessTimeDraftTouched, fileInputRef, submitting, reviewActionBusy, sessionSwitchBusy, createMessage, nextTick, scrollToBottom, persistSessionState, clearAttachedFiles, adjustComposerTextareaHeight, buildComposerBusinessTimeContext, openTravelCalculator, lockSuggestedActionMessage, submitExistingComposer: submitComposerInternal, currentUser, refreshCurrentUserFromBackend, toast }) function openTravelCalculator() { if (!canShowTravelCalculator.value) { closeTravelCalculator() return false } return openTravelCalculatorInternal() } function toggleTravelCalculator() { if (!canShowTravelCalculator.value) { closeTravelCalculator() return false } return toggleTravelCalculatorInternal() } function submitTravelCalculator() { if (!canShowTravelCalculator.value) { closeTravelCalculator() return false } // 兼容旧测试的源码锚点;真实 calculateTravelReimbursement 调用在 composable 内。 // calculateTravelReimbursement({ grade: String(user.grade || '').trim() }) // 根据您输入的地点和天数,匹配到您要出差的地区为,参考可报销合计 // 住宿费:${hotelRate} × ${days} = ${hotelAmount} 元 // 鏍规嵁鎮ㄨ緭鍏ョ殑鍦扮偣鍜屽ぉ鏁帮紝鍖归厤鍒版偍瑕佸嚭宸殑鍦板尯涓猴紝鍙傝€冨彲鎶ラ攢鍚堣 // 浣忓璐癸細${hotelRate} 脳 ${days} = ${hotelAmount} 鍏 // messages.value.push(createMessage('assistant', buildTravelCalculatorResultText(payload) return submitTravelCalculatorInternal() } watch(canShowTravelCalculator, (visible) => { if (!visible && travelCalculatorOpen.value) { closeTravelCalculator() } }) const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW) const shortcuts = computed(() => { if (isStewardSession.value) { return [] } const accessibleModes = filterAssistantSessionModes(ASSISTANT_SESSION_MODE_OPTIONS, currentUser.value) .filter((mode) => mode.key !== SESSION_TYPE_STEWARD) const visibleModes = props.entrySource === 'budget' ? accessibleModes.filter((mode) => mode.key === SESSION_TYPE_BUDGET) : accessibleModes return visibleModes.map((mode) => ({ label: mode.label, icon: mode.icon, action: 'switch_view', targetSessionType: mode.key, active: mode.key === activeSessionType.value })) }) useTravelReimbursementCreateViewLifecycle({ REVIEW_DRAWER_MODE_DOCUMENTS, REVIEW_DRAWER_MODE_FLOW, REVIEW_DRAWER_MODE_REVIEW, REVIEW_DRAWER_MODE_RISK, SESSION_TYPE_EXPENSE, activeFlowSteps, activeReviewPanelScope, activeReviewPayload, activeSessionType, adjustComposerTextareaHeight, attachedFiles, clearExpenseSessionForDeletedClaim, clearStewardThinkingTimers: () => clearStewardThinkingTimers(), closeAfterBusy, composerDraft, composerFilesExpanded, composerUploadIntent, conversationId, currentInsight, currentUser, draftClaimId, guidedFlowState, handleComposerDatePickerOutside, hasInsightPanelContent, insightPanelCollapsed, linkedRequest, maybeFinalizeDeferredClose, mergeFilesWithLimit, messages, persistSessionState, props, rememberFilePreviews, resetReviewDrawerFromPayload, resolveActiveClaimId, restorePersistedDraftAttachmentPreviews, reviewDocumentDrawerAvailable, reviewDrawerMode, reviewFilePreviews, reviewFlowDrawerAvailable, reviewRiskDrawerAvailable, scrollToBottom, startFlowTick, stopAttachmentRuntime, stopFlowRuntime, submitComposer, toast, workbenchVisible }) function scrollToBottom() { const scrollOnce = () => { const list = messageListRef.value?.$el || messageListRef.value if (!list) { return false } list.scrollTop = list.scrollHeight return true } nextTick(() => { if (scrollOnce()) { return } requestAnimationFrame(() => { scrollOnce() requestAnimationFrame(scrollOnce) }) }) } function handleAssistantModalAfterEnter() { scrollToBottom() requestAnimationFrame(() => { scrollToBottom() }) } function resetCurrentSessionState() { const emptyState = buildEmptySessionState(activeSessionType.value) sessionSnapshots.value[activeSessionType.value] = emptyState resetGuidedFlowState() applySessionState(emptyState) resetFlowRun({ startedAt: 0, openDrawer: false }) } function clearExpenseSessionForDeletedClaim(claimId) { const normalizedClaimId = String(claimId || '').trim() if (!normalizedClaimId) { return } const expenseSnapshot = sessionSnapshots.value[SESSION_TYPE_EXPENSE] const snapshotMatchesDeletedClaim = String(expenseSnapshot?.draftClaimId || '').trim() === normalizedClaimId const currentMatchesDeletedClaim = activeSessionType.value === SESSION_TYPE_EXPENSE && String(resolveActiveClaimId() || '').trim() === normalizedClaimId if (!snapshotMatchesDeletedClaim && !currentMatchesDeletedClaim) { return } clearAssistantSessionSnapshot(resolveCurrentUserId(), SESSION_TYPE_EXPENSE) if (currentMatchesDeletedClaim) { resetCurrentSessionState() toast('该草稿单据已删除,相关财务助手会话已清空。') return } sessionSnapshots.value[SESSION_TYPE_EXPENSE] = buildEmptySessionState(SESSION_TYPE_EXPENSE) } function adjustComposerTextareaHeight() { if (!composerTextareaRef.value) return const textarea = composerTextareaRef.value textarea.style.height = 'auto' const styles = window.getComputedStyle(textarea) const lineHeight = Number.parseFloat(styles.lineHeight) || 20 const verticalPadding = Number.parseFloat(styles.paddingTop || '0') + Number.parseFloat(styles.paddingBottom || '0') const minHeight = COMPOSER_TEXTAREA_HEIGHT const maxHeight = lineHeight * COMPOSER_MAX_ROWS + verticalPadding const nextHeight = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight)) textarea.style.height = `${nextHeight}px` textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden' } function handleComposerInput() { adjustComposerTextareaHeight() } function handleComposerEnter(event) { if (event?.isComposing || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) { return } submitComposer() } 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) } const { submitStewardPlan, clearStewardThinkingTimers } = useStewardPlanFlow({ activeSessionType, attachedFiles, composerDraft, currentUser, fileInputRef, messages, createMessage, fetchStewardPlan, fetchStewardPlanStream, nextTick, persistSessionState, replaceMessage, scrollToBottom, adjustComposerTextareaHeight, executeStewardSuggestedAction: (message, action) => handleSuggestedAction(message, action), submitting, reviewActionBusy, sessionSwitchBusy, toast }) function lockSuggestedActionMessage(message, action) { const messageId = String(message?.id || '').trim() const targetMessage = messages.value.find((item) => String(item.id || '') === messageId) || message if (!targetMessage || targetMessage.suggestedActionsLocked) { return false } const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const selectedLabel = String(action?.label || actionPayload.expense_type_label || '').trim() const nextMeta = Array.isArray(targetMessage.meta) ? targetMessage.meta.filter((item) => item !== '等待选择场景') : [] const selectedMeta = selectedLabel ? `已选择${selectedLabel}` : '已选择场景' targetMessage.suggestedActionsLocked = true targetMessage.selectedSuggestedActionKey = buildSuggestedActionKey(action) targetMessage.selectedSuggestedActionLabel = selectedLabel targetMessage.meta = Array.from(new Set([...nextMeta, selectedMeta])) persistSessionState() return true } let handleReviewActionInternal = null const { appendExpenseQueryRiskToConversation, appendReviewRiskBriefToConversation, buildApplicationDraftSummaryItems, buildApplicationPreviewFooterText, buildMessageBubbleClass, buildReviewNextStepRichCopyForMessage, canOpenDraftDetail, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, copyAssistantMessage, isApplicationDraftPayload, isMessageFeedbackSelected, openApplicationDraftDetail, openReviewNextStepConfirm, queryDraftByClaimNo, resolveApplicationDraftStatusLabel, resolveApplicationPreviewMissingFields, resolveReimbursementDraftClaimNo, shouldShowAssistantMessageActions, shouldShowDraftSavedCard, speakAssistantMessage, submitOperationFeedbackForMessage } = useTravelReimbursementMessageActions({ activeSessionType, buildMessageActionRows: (message) => { const applicationPreview = message?.applicationPreview ? normalizeApplicationPreview(message.applicationPreview) : null if (!applicationPreview?.fields) { return [] } return resolveApplicationPreviewRows({ applicationPreview }).map((row) => `${row.label}:${row.value || '待补充'}` ) }, conversationId, createMessage, currentInsight, currentUser, draftClaimId, emit, getHandleReviewActionInternal: () => handleReviewActionInternal, latestReviewMessage, linkedRequest, messages, nextStepConfirmDialog, nextTick, persistSessionState, props, resolveActiveClaimId, resolveCurrentUserId, reviewActionBusy, router, scrollToBottom, submitComposer, submitting, toast }) const { handleSuggestedAction, isSuggestedActionSelected, runShortcut } = useTravelReimbursementSuggestedActions({ applicationPreviewEditor, attachedFiles, buildExpenseSceneSelectionActions, buildExpenseSceneSelectionMessage, commitApplicationPreviewEditor, composerDraft, composerFilesExpanded, composerTextareaRef, createMessage, currentUser, emit, handleGuidedShortcut, handleGuidedSuggestedAction, handleSceneSelectionApplicationGate, lockSuggestedActionMessage, mergeFilesWithLimit, messages, nextTick, openApplicationPreviewEditor, persistSessionState, resolveApplicationPreviewMissingFields, reviewActionBusy, router, scrollToBottom, sessionSwitchBusy, startExpenseSceneSelectionAfterIntentConfirmation, submitComposer, submitComposerInternal, submitting, switchSessionType, toast, adjustComposerTextareaHeight }) function toggleInsightPanel() { if (!hasInsightPanelContent.value) { return } insightPanelCollapsed.value = !insightPanelCollapsed.value } function switchReviewDrawerMode(mode) { if (reviewDrawerMode.value === mode) { return } reviewDrawerMode.value = mode } function switchToReviewOverviewDrawer() { if (!reviewOverviewDrawerAvailable.value) { return } switchReviewDrawerMode(REVIEW_DRAWER_MODE_REVIEW) } function toggleReviewDocumentDrawer() { if (!reviewDocumentDrawerAvailable.value) { return } switchReviewDrawerMode(REVIEW_DRAWER_MODE_DOCUMENTS) } function toggleReviewRiskDrawer() { if (!reviewRiskDrawerAvailable.value) { return } switchReviewDrawerMode(REVIEW_DRAWER_MODE_RISK) } function toggleReviewFlowDrawer() { if (!reviewFlowDrawerAvailable.value) { return } switchReviewDrawerMode(REVIEW_DRAWER_MODE_FLOW) } const { closeApplicationSubmitConfirm, confirmApplicationSubmit, handleApplicationSubmitConfirmationText, handleStewardRuntimeDecision, openApplicationSubmitConfirm, resolveStewardMissingFieldItems } = useTravelReimbursementStewardRuntime({ activeSessionType, applicationSubmitConfirmDialog, attachedFiles, composerDraft, createMessage, currentUser, emit, handleSuggestedAction, isStewardSession, linkedRequest, messages, nextTick, persistSessionState, props, reviewActionBusy, scrollToBottom, sessionSwitchBusy, submitComposer, submitStewardPlan, submitting, toast, adjustComposerTextareaHeight, resolveCurrentUserId }) function isWorkbenchBusy() { return submitting.value || reviewActionBusy.value || sessionSwitchBusy.value } function maybeFinalizeDeferredClose() { if (!closeAfterBusy.value || workbenchVisible.value || isWorkbenchBusy()) { return } closeAfterBusy.value = false emit('close') } function requestCloseWorkbench() { persistSessionState() closeAfterBusy.value = isWorkbenchBusy() workbenchVisible.value = false } function emitCloseAfterLeave() { if (workbenchVisible.value) { return } if (closeAfterBusy.value && isWorkbenchBusy()) { return } closeAfterBusy.value = false emit('close') } function openExpenseQueryRecord(record) { const claimId = String(record?.claimId || '').trim() if (!claimId) { return } router.push({ name: 'app-document-detail', params: { requestId: claimId } }) emit('close') } async function handleExpenseQueryRecordClick(message, record) { if (message?.queryPayload?.selectionMode !== 'draft_association') { openExpenseQueryRecord(record) return } if (message.querySelectionLocked || message.queryPayload.selectionLocked || submitting.value || reviewActionBusy.value) { return } const claimId = String(record?.claimId || '').trim() if (!claimId) { return } const files = Array.from(attachedFiles.value || []) if (!files.length) { toast('本次上传的附件已不在当前会话中,请重新选择附件后再关联草稿。') return } message.querySelectionLocked = true message.selectedQueryRecordId = claimId message.queryPayload.selectionLocked = true message.queryPayload.selectedClaimId = claimId draftClaimId.value = claimId persistSessionState() await submitComposer({ rawText: `将本次上传的 ${files.length} 份票据关联到报销草稿 ${record.claimNo}`, userText: `关联到草稿 ${record.claimNo}`, pendingText: `已选择草稿 ${record.claimNo},正在识别并归集附件...`, files, uploadDisposition: 'continue_existing', extraContext: { draft_claim_id: claimId, selected_claim_id: claimId, selected_claim_no: String(record?.claimNo || '').trim() } }) } function setExpenseQueryPage(message, page) { if (!message?.queryPayload) { return } const totalPages = getExpenseQueryTotalPages(message.queryPayload) const nextPage = Math.min(Math.max(1, Number(page || 1)), totalPages) message.queryPayload.currentPage = nextPage } function shiftExpenseQueryPage(message, delta) { if (!message?.queryPayload) { return } setExpenseQueryPage(message, getExpenseQueryActivePage(message.queryPayload) + Number(delta || 0)) } function openDeleteSessionDialog() { if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || sessionSwitchBusy.value) { return } deleteSessionDialogOpen.value = true } function closeDeleteSessionDialog() { if (deleteSessionBusy.value) { return } deleteSessionDialogOpen.value = false } async function confirmDeleteCurrentSession() { if (deleteSessionBusy.value || sessionSwitchBusy.value) { return } deleteSessionBusy.value = true try { if (conversationId.value) { await deleteConversation(conversationId.value, resolveCurrentUserId()) } clearAssistantSessionSnapshot(resolveCurrentUserId(), activeSessionType.value) resetCurrentSessionState() deleteSessionDialogOpen.value = false toast('当前会话已删除。') } catch (error) { toast(error?.message || '删除当前会话失败,请稍后重试。') } finally { deleteSessionBusy.value = false } } const reviewActionRuntime = useTravelReimbursementReviewActions({ activeReviewPayload, buildDraftSavedPayload, buildLocalReviewCompletionMessage, buildLocalReviewSavedMessage, buildReviewCorrectionMessage, buildReviewDocumentCorrectionContext, buildReviewDocumentCorrectionMessage, buildReviewFormValues, buildReviewRiskItems, buildReviewSubmitUserText, buildLocallySyncedReviewPayload, cloneReviewDocumentDrafts, cloneReviewEditFields, commitInlineReviewEditor, createMessage, currentInsight, currentUser, emit, latestReviewMessage, linkedRequest, mergeInlineReviewFields, messages, nextTick, reviewActionBusy, reviewDocumentBaseDrafts, reviewDocumentDrafts, reviewHasUnsavedChanges, reviewInlineBaseFields, reviewInlineBaseForm, reviewInlineEditorKey, reviewInlineForm, reviewInlinePendingFiles, scrollToBottom, sessionSwitchBusy, submitComposer, submitting }) handleReviewActionInternal = reviewActionRuntime.handleReviewActionInternal const { handleSaveDraftDirectlyInternal, saveInlineReviewChangesInternal } = reviewActionRuntime function saveInlineReviewChanges() { if ( !activeReviewPayload.value || !reviewHasUnsavedChanges.value || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value ) return return saveInlineReviewChangesInternal() } function askHotKnowledgeQuestion(question) { const normalizedQuestion = String(question || '').trim() if (!normalizedQuestion || !isKnowledgeSession.value || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) { return } submitComposer({ rawText: normalizedQuestion, userText: normalizedQuestion, pendingText: '正在整理财务知识答案...' }) } async function submitComposer(options = {}) { // resolvedUploadDisposition === 'continue_existing' // buildReviewFormContextFromPayload( // activeReviewPayload.value, // reviewInlineForm.value // ) // extraContext.review_form_values // inheritedReviewContext.business_time_context // extraContext.business_time_context = inheritedReviewContext.business_time_context // submitting.value = true // recognizeOcrFiles(files) // submitting.value = false if (await handleStewardRuntimeDecision(options)) { return null } if (await handleApplicationSubmitConfirmationText(options)) { return null } if (isStewardSession.value && !options.skipStewardPlan && await submitStewardPlan(options)) { return null } if (await handleGuidedComposerSubmit(options)) { return null } return submitComposerInternal(options) } async function handleAssistantMarkdownClick(event, message) { const anchor = event?.target?.closest?.('a') if (!anchor || !message || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) { return } const href = String(anchor.getAttribute('href') || '').trim() if (href === APPLICATION_SUBMIT_HREF) { event.preventDefault() openApplicationSubmitConfirm(message) return } if (href === REVIEW_NEXT_STEP_HREF) { event.preventDefault() openReviewNextStepConfirm(message) return } if (href.startsWith(REVIEW_RISK_PANEL_HREF_PREFIX)) { event.preventDefault() if (reviewRiskDrawerAvailable.value) { switchReviewDrawerMode(REVIEW_DRAWER_MODE_RISK) } else { toast('当前没有需要额外处理的风险信息。') } return } if (href === REVIEW_QUICK_EDIT_HREF) { event.preventDefault() if (reviewOverviewDrawerAvailable.value) { switchReviewDrawerMode(REVIEW_DRAWER_MODE_REVIEW) toast('已打开右侧核对信息,可以直接修改当前单据。') } return } if (href.startsWith('/app/')) { event.preventDefault() router.push(href) return } if (href !== ATTACHMENT_ASSOCIATION_CONFIRM_HREF) { return } event.preventDefault() reviewActionBusy.value = true try { await confirmPendingAttachmentAssociationInternal(message) } finally { reviewActionBusy.value = false } } async function handleReviewAction(message, action) { const actionType = String(action?.action_type || '').trim() if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return return handleReviewActionInternal(message, action) } async function handleSaveDraftDirectly(message, actionType = 'save_draft') { return handleSaveDraftDirectlyInternal(message, actionType) } function isDraftSavedReviewMessage(message) { if (!message?.reviewPayload) { return false } return Boolean( String(message?.draftPayload?.claim_no || message?.draftPayload?.claim_id || '').trim() || String(draftClaimId.value || '').trim() || String(resolveActiveClaimId() || '').trim() ) } function buildReviewPlainFollowupForMessage(message) { return buildReviewPlainFollowupCopy(message?.reviewPayload, { savedDraft: isDraftSavedReviewMessage(message) }) } function canUseInlineSaveDraft(message) { if (!message?.reviewPayload || isDraftSavedReviewMessage(message)) { return false } return Boolean(resolveReviewSaveDraftAction(message.reviewPayload)) } async function handleInlineSaveDraft(message) { if ( !canUseInlineSaveDraft(message) || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value ) { return } await handleSaveDraftDirectly(message, 'save_draft') } const { messageItemUi, insightPanelUi } = useTravelReimbursementCreateViewUi({ ASSISTANT_DISPLAY_NAME, DATE_INPUT_FORMAT, REVIEW_SCENE_OPTIONS, REVIEW_SCENE_OTHER_OPTION, activeFlowSteps, activeReviewDocument, activeReviewDocumentIndex, activeReviewDocumentPreview, activeReviewPayload, activeSessionType, aiAvatar, appendExpenseQueryRiskToConversation, appendReviewRiskBriefToConversation, applicationPreviewEditor, buildApplicationDraftSummaryItems, buildApplicationPreviewFooterText, buildExpenseQueryHint, buildExpenseQueryWindowLabel, buildMessageBubbleClass, buildReviewMainMessageText, buildReviewNextStepRichCopyForMessage, buildReviewPlainFollowupForMessage, buildReviewPrimaryButtonLabel, canApplyApplicationPreviewDateSelection, canOpenDraftDetail, canPreviewActiveReviewDocument, canUseInlineSaveDraft, clearInlineReviewFieldError, commitApplicationPreviewDateEditor, commitApplicationPreviewEditor, commitInlineReviewEditor, copyAssistantMessage, currentInsight, currentIntentLabel, deleteSessionBusy, flowOverallStatusText, flowOverallStatusTone, flowRefreshBusy, flowRunId, flowTotalDurationText, formatFlowStepDuration, getExpenseQueryActivePage, getExpenseQueryTotalPages, getExpenseQueryVisibleRecords, goReviewDocument, handleApplicationPreviewEditorKeydown, handleAssistantMarkdownClick, handleExpenseQueryRecordClick, handleInlineSaveDraft, handleReviewAction, handleSuggestedAction, hotKnowledgeQuestions, isApplicationDraftPayload, isApplicationPreviewDateEditorOpen, isApplicationPreviewEditing, isKnowledgeSession, isMessageFeedbackSelected, isReviewDocumentDrawer, isReviewFlowDrawer, isReviewOverviewDrawer, isReviewRiskDrawer, isSuggestedActionSelected, openActiveReviewDocumentPreview, openApplicationDraftDetail, openApplicationPreviewEditorFromUi, openInlineReviewEditor, refreshFlowRunDetail, renderMarkdown, resolveApplicationDraftStatusLabel, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, resolveApplicationPreviewMissingFields, resolveApplicationPreviewRows, resolveFlowStepDetail, resolveFlowStepStatusLabel, resolveKnowledgeRankLabel, resolveKnowledgeRankTone, resolveReimbursementDraftClaimNo, resolveReviewFooterActions, resolveStewardMissingFieldItems, reviewActionBusy, reviewCategoryOptions, reviewDocumentCount, reviewDocumentDrawerAvailable, reviewDocumentDrawerIcon, reviewDrawerMode, reviewDrawerTitle, reviewFactCards, reviewFlowDrawerAvailable, reviewFlowDrawerIcon, reviewHasUnsavedChanges, reviewInlineEditorKey, reviewInlineErrors, reviewInlineForm, reviewInlinePendingFiles, reviewIntentText, reviewOtherCategoryOpen, reviewOtherCategoryOptions, reviewOverviewDrawerAvailable, reviewPanelConfidence, reviewRiskDrawerAvailable, reviewRiskDrawerIcon, reviewRiskEmpty, reviewRiskItems, reviewRiskSummary, reviewSelectedOtherCategory, runShortcut, saveInlineReviewChanges, selectInlineScene, selectReviewCategory, selectReviewOtherCategory, sessionSwitchBusy, setApplicationPreviewDateMode, setExpenseQueryPage, shiftExpenseQueryPage, shouldShowAssistantMessageActions, shouldShowDraftSavedCard, showInsightPanel, speakAssistantMessage, submitting, submitOperationFeedbackForMessage, switchToReviewOverviewDrawer, toggleReviewDocumentDrawer, toggleReviewFlowDrawer, toggleReviewRiskDrawer, userAvatar, visibleFlowSteps }) return { emit, messageItemUi, insightPanelUi, ASSISTANT_DISPLAY_NAME, aiAvatar, userAvatar, fileInputRef, composerTextareaRef, messageListRef, composerDraft, composerDatePickerOpen, composerDateMode, composerSingleDate, composerRangeStartDate, composerRangeEndDate, composerBusinessTimeTags, composerCanApplyDateSelection, toggleComposerDatePicker, closeComposerDatePicker, setComposerDateMode, handleComposerDateInputChange, removeComposerBusinessTimeTag, flowSteps, flowRunId, flowRefreshBusy, completedFlowStepCount, flowOverallStatusTone, flowOverallStatusText, flowTotalDurationText, attachedFiles, composerFilesExpanded, visibleAttachedFiles, hiddenAttachedFileCount, submitting, sessionSwitchBusy, messages, currentInsight, linkedRequest, canSubmit, activeSessionType, isKnowledgeSession, isStewardSession, showStewardInitialRecognition, hotKnowledgeQuestions, hasInsightPanelContent, showInsightPanel, insightPanelToggleLabel, assistantHeaderTitle, assistantHeaderDescription, composerPlaceholder, currentIntentLabel, canDeleteCurrentSession, latestReviewMessage, activeReviewPayload, activeReviewPanelScope, activeReviewFilePreviews, reviewDrawerMode, isReviewOverviewDrawer, isReviewDocumentDrawer, isReviewRiskDrawer, isReviewFlowDrawer, reviewDrawerTitle, reviewOverviewDrawerAvailable, reviewDocumentDrawerAvailable, reviewRiskDrawerAvailable, reviewFlowDrawerAvailable, reviewDocumentDrawerLabel, reviewDocumentDrawerIcon, reviewRiskDrawerLabel, reviewRiskDrawerIcon, reviewFlowDrawerLabel, reviewFlowDrawerIcon, activeReviewDocument, activeReviewDocumentIndex, activeReviewDocumentPreview, canPreviewActiveReviewDocument, reviewIntentText, reviewFactCards, reviewCategoryOptions, reviewOtherCategoryOptions, reviewSelectedOtherCategory, reviewInlineDirty, reviewInlineForm, reviewInlineEditorKey, reviewInlineErrors, reviewOtherCategoryOpen, reviewInlinePendingFiles, DATE_INPUT_FORMAT, REVIEW_SCENE_OTHER_OPTION, REVIEW_SCENE_OPTIONS, REVIEW_OTHER_CATEGORY_OPTIONS, workbenchVisible, reviewPanelConfidence, reviewRiskSummary, reviewRiskItems, reviewRiskEmpty, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewDocumentCount, reviewDocumentDirty, reviewHasUnsavedChanges, travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, canShowTravelCalculator, deleteSessionDialogOpen, applicationSubmitConfirmDialog, applicationPreviewEditor, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts, resolveReviewMissingSlotCards, resolveReviewRiskBriefs, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewPlainFollowupCopy, buildReviewPlainFollowupForMessage, buildReviewNextStepRichCopyForMessage, buildMessageBubbleClass, resolveReviewFooterActions, resolveReviewSaveDraftAction, buildReviewPrimaryButtonLabel, buildReviewMainMessageText, renderMarkdown, buildExpenseQueryWindowLabel, buildExpenseQueryHint, getExpenseQueryActivePage, getExpenseQueryTotalPages, getExpenseQueryVisibleRecords, resolveDocumentPreview, triggerFileUpload, applyComposerDateSelection, handleFilesChange, handleComposerInput, handleComposerEnter, runShortcut, runWelcomeQuickAction: runShortcut, handleSuggestedAction, isSuggestedActionSelected, askHotKnowledgeQuestion, resolveKnowledgeRankLabel, resolveKnowledgeRankTone, refreshFlowRunDetail, formatFlowStepDuration, resolveFlowStepStatusLabel, resolveFlowStepDetail, toggleInsightPanel, openTravelCalculator, toggleTravelCalculator, closeTravelCalculator, submitTravelCalculator, switchToReviewOverviewDrawer, toggleReviewDocumentDrawer, toggleReviewRiskDrawer, toggleReviewFlowDrawer, toggleAttachedFilesExpanded, removeAttachedFile, clearAttachedFiles, requestCloseWorkbench, emitCloseAfterLeave, handleAssistantModalAfterEnter, openExpenseQueryRecord, handleExpenseQueryRecordClick, setExpenseQueryPage, shiftExpenseQueryPage, openDeleteSessionDialog, closeDeleteSessionDialog, confirmDeleteCurrentSession, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, clearInlineReviewFieldError, selectInlineScene, selectReviewCategory, selectReviewOtherCategory, queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, isApplicationPreviewEditing, isApplicationPreviewDateEditorOpen, openApplicationPreviewEditor: openApplicationPreviewEditorFromUi, commitApplicationPreviewEditor, commitApplicationPreviewDateEditor, cancelApplicationPreviewEditor, setApplicationPreviewDateMode, canApplyApplicationPreviewDateSelection, handleApplicationPreviewEditorKeydown, buildApplicationPreviewFooterText, isApplicationDraftPayload, resolveApplicationDraftStatusLabel, buildApplicationDraftSummaryItems, shouldShowDraftSavedCard, resolveReimbursementDraftClaimNo, openApplicationDraftDetail, shouldShowAssistantMessageActions, copyAssistantMessage, speakAssistantMessage, isMessageFeedbackSelected, submitOperationFeedbackForMessage, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft } } }