Files
X-Financial/web/src/composables/workbenchAiMode/useWorkbenchAiExpenseFlow.js

187 lines
7.0 KiB
JavaScript
Raw Normal View History

import { fetchExpenseClaims } from '../../services/reimbursements.js'
import {
applyAiExpenseAnswer,
buildAiExpenseStepPrompt,
buildAiExpenseSummary,
createAiExpenseDraft,
isAiExpenseDraftComplete
} from '../../utils/aiExpenseDraftModel.js'
import {
buildExpenseSceneSelectionMessage,
SESSION_TYPE_EXPENSE
} from '../../views/scripts/travelReimbursementConversationModel.js'
import { buildExpenseSceneSelectionActions } from '../../utils/expenseAssistantActions.js'
import {
buildRequiredApplicationActions,
buildRequiredApplicationMissingText,
buildRequiredApplicationSelectionText,
filterRequiredApplicationCandidates
} from '../../views/scripts/travelReimbursementApplicationLinkModel.js'
export { SESSION_TYPE_EXPENSE }
export function useWorkbenchAiExpenseFlow({
activateInlineConversation,
aiExpenseDraft,
assistantDraft,
clearAiModeFiles,
closeWorkbenchDatePicker,
conversationMessages,
conversationStarted,
createInlineMessage,
currentUser,
persistCurrentConversation,
pushInlineUserMessage,
removeWorkbenchDateTag,
resolveLatestInlineUserPrompt,
scrollInlineConversationToBottom,
startAiApplicationPreview
}) {
function pushInlineExpenseSceneSelectionPrompt(originalMessage, selectedLabel = '') {
const sourceText = String(originalMessage || '我要报销').trim()
if (!conversationStarted.value) {
activateInlineConversation({
title: String(selectedLabel || sourceText || '报销').trim().slice(0, 18) || '报销'
})
}
assistantDraft.value = ''
removeWorkbenchDateTag()
closeWorkbenchDatePicker()
conversationMessages.value.push(createInlineMessage('user', String(selectedLabel || sourceText).trim()))
conversationMessages.value.push(createInlineMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), {
suggestedActions: buildExpenseSceneSelectionActions(sourceText)
}))
persistCurrentConversation()
scrollInlineConversationToBottom()
}
function startAiApplicationPreviewFromAction(payload = {}, fallbackLabel = '') {
const expenseType = String(payload.expense_type || '').trim()
const expenseTypeLabel = String(payload.expense_type_label || fallbackLabel || '').trim()
return startAiApplicationPreview(
expenseType,
expenseTypeLabel,
payload.carry_text || resolveLatestInlineUserPrompt()
)
}
function startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement) {
if (!conversationStarted.value) {
activateInlineConversation({ title: String(expenseTypeLabel || '报销').trim().slice(0, 18) || '报销' })
}
assistantDraft.value = ''
removeWorkbenchDateTag()
closeWorkbenchDatePicker()
clearAiModeFiles()
pushInlineUserMessage(`选择${expenseTypeLabel || expenseType || '报销'}`)
if (requiresApplicationBeforeReimbursement) {
void resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel)
return
}
const draft = createAiExpenseDraft(expenseType, expenseTypeLabel)
aiExpenseDraft.value = draft
conversationMessages.value.push(createInlineMessage('assistant', buildAiExpenseStepPrompt(draft)))
persistCurrentConversation()
scrollInlineConversationToBottom()
}
function advanceAiExpenseDraft(answer, files = []) {
const fileNames = Array.from(files || [])
pushInlineUserMessage(answer || (fileNames.length ? `上传 ${fileNames.length} 份附件` : ''))
assistantDraft.value = ''
clearAiModeFiles()
const next = applyAiExpenseAnswer(aiExpenseDraft.value, answer, fileNames)
aiExpenseDraft.value = next
if (isAiExpenseDraftComplete(next)) {
conversationMessages.value.push(createInlineMessage('assistant', `${buildAiExpenseSummary(next)}\n\n如果哪一项需要修改,直接告诉我;确认无误后我再帮你生成报销草稿。`))
aiExpenseDraft.value = null
} else {
conversationMessages.value.push(createInlineMessage('assistant', buildAiExpenseStepPrompt(next)))
}
persistCurrentConversation()
scrollInlineConversationToBottom()
}
async function resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel) {
let claims = null
try {
claims = await fetchExpenseClaims()
} catch {
aiExpenseDraft.value = null
conversationMessages.value.push(createInlineMessage('assistant', '查询可关联申请单时出现异常,请稍后再试,我先暂停这次报销流程。'))
persistCurrentConversation()
scrollInlineConversationToBottom()
return
}
const candidates = filterRequiredApplicationCandidates(claims, expenseType, currentUser.value || {})
aiExpenseDraft.value = createAiExpenseDraft(expenseType, expenseTypeLabel)
if (!candidates.length) {
conversationMessages.value.push(createInlineMessage('assistant', buildRequiredApplicationMissingText(expenseType), {
suggestedActions: [{
label: '确认发起出差申请',
description: '生成完整申请表,并预填已识别的时间、地点和事由',
icon: 'mdi mdi-file-plus-outline',
action_type: 'ai_application_start_inline',
payload: {
expense_type: expenseType,
expense_type_label: expenseTypeLabel
}
}]
}))
persistCurrentConversation()
scrollInlineConversationToBottom()
return
}
conversationMessages.value.push(createInlineMessage('assistant', buildRequiredApplicationSelectionText(expenseType, candidates), {
suggestedActions: buildRequiredApplicationActions(candidates, 'select_required_application')
}))
persistCurrentConversation()
scrollInlineConversationToBottom()
}
function linkAiExpenseApplication(application = {}) {
const draft = aiExpenseDraft.value
if (!draft) {
return
}
const claimNo = String(application.application_claim_no || '').trim()
pushInlineUserMessage(`关联申请单 ${claimNo}`.trim())
const linked = {
...draft,
applicationClaim: application,
values: {
...draft.values,
reason: String(application.application_reason || '').trim(),
location: String(application.application_location || '').trim(),
time_range: String(application.application_business_time || '').trim(),
amount: String(application.application_amount_label || application.application_amount || '').trim()
},
stepKey: 'attachments'
}
aiExpenseDraft.value = linked
conversationMessages.value.push(createInlineMessage('assistant', [
`已关联申请单${claimNo ? ` ${claimNo}` : ''},事由、时间、地点、金额我先用申请单的内容预填了。`,
'',
'再确认一下票据:可以现在上传,或回复“稍后上传”。'
].join('\n')))
persistCurrentConversation()
scrollInlineConversationToBottom()
}
return {
advanceAiExpenseDraft,
linkAiExpenseApplication,
pushInlineExpenseSceneSelectionPrompt,
startAiApplicationPreviewFromAction,
startAiExpenseDraft
}
}