refactor(frontend): split large reimbursement and audit modules
This commit is contained in:
335
web/src/views/scripts/useTravelReimbursementSessionState.js
Normal file
335
web/src/views/scripts/useTravelReimbursementSessionState.js
Normal file
@@ -0,0 +1,335 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
||||
import {
|
||||
clearAssistantSessionSnapshot,
|
||||
readAssistantSessionSnapshot,
|
||||
writeAssistantSessionSnapshot
|
||||
} from '../../utils/assistantSessionSnapshot.js'
|
||||
import { buildReviewFilePreviewsFromMessages } from './travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
SESSION_TYPE_EXPENSE,
|
||||
SESSION_TYPE_KNOWLEDGE,
|
||||
buildInitialInsightFromConversation,
|
||||
buildWelcomeInsight,
|
||||
createWelcomeAssistantMessage,
|
||||
hasMeaningfulSessionMessages,
|
||||
normalizeInitialConversationMessages,
|
||||
normalizeSnapshotMessages,
|
||||
resolveInitialConversationId,
|
||||
resolveInitialDraftClaimId,
|
||||
resolveInitialSessionType,
|
||||
serializeSessionMessages,
|
||||
shouldPreferPersistedSessionState
|
||||
} from './travelReimbursementConversationModel.js'
|
||||
|
||||
export function useTravelReimbursementSessionState({
|
||||
props,
|
||||
currentUser,
|
||||
linkedRequest,
|
||||
toast,
|
||||
composerDraft,
|
||||
uploadDecisionDialogOpen,
|
||||
adjustComposerTextareaHeight,
|
||||
scrollToBottom,
|
||||
getSessionRuntimeRefs = () => ({})
|
||||
}) {
|
||||
function buildConversationSessionState(conversation, fallbackSessionType = SESSION_TYPE_EXPENSE) {
|
||||
const sessionType = resolveInitialSessionType(conversation) || fallbackSessionType
|
||||
const restoredMessages = normalizeInitialConversationMessages(conversation)
|
||||
const initialInsight = buildInitialInsightFromConversation(conversation)
|
||||
const restoredReviewFilePreviews = buildReviewFilePreviewsFromMessages(restoredMessages)
|
||||
|
||||
return {
|
||||
sessionType,
|
||||
messages: restoredMessages.length
|
||||
? restoredMessages
|
||||
: [createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, sessionType, currentUser.value)],
|
||||
conversationId: resolveInitialConversationId(conversation),
|
||||
draftClaimId: resolveInitialDraftClaimId(conversation),
|
||||
currentInsight:
|
||||
initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType, currentUser.value),
|
||||
reviewFilePreviews: restoredReviewFilePreviews,
|
||||
composerDraft: '',
|
||||
attachedFiles: [],
|
||||
composerFilesExpanded: false,
|
||||
composerUploadIntent: '',
|
||||
insightPanelCollapsed: false
|
||||
}
|
||||
}
|
||||
|
||||
function buildEmptySessionState(sessionType) {
|
||||
return {
|
||||
sessionType,
|
||||
messages: [
|
||||
createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, sessionType, currentUser.value)
|
||||
],
|
||||
conversationId: '',
|
||||
draftClaimId: '',
|
||||
currentInsight: buildWelcomeInsight(
|
||||
props.entrySource,
|
||||
linkedRequest.value,
|
||||
sessionType,
|
||||
currentUser.value
|
||||
),
|
||||
reviewFilePreviews: [],
|
||||
composerDraft: '',
|
||||
attachedFiles: [],
|
||||
composerFilesExpanded: false,
|
||||
composerUploadIntent: '',
|
||||
insightPanelCollapsed: false
|
||||
}
|
||||
}
|
||||
|
||||
function buildPersistedSessionState(snapshot, fallbackSessionType = SESSION_TYPE_EXPENSE) {
|
||||
const state = snapshot?.state && typeof snapshot.state === 'object' ? snapshot.state : null
|
||||
if (!state) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sessionType = String(state.sessionType || snapshot.sessionType || fallbackSessionType || '').trim() || SESSION_TYPE_EXPENSE
|
||||
const restoredMessages = normalizeSnapshotMessages(state.messages)
|
||||
if (
|
||||
!hasMeaningfulSessionMessages(restoredMessages)
|
||||
&& !String(state.conversationId || '').trim()
|
||||
&& !String(state.draftClaimId || '').trim()
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
sessionType,
|
||||
messages: restoredMessages.length
|
||||
? restoredMessages
|
||||
: [createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, sessionType, currentUser.value)],
|
||||
conversationId: String(state.conversationId || '').trim(),
|
||||
draftClaimId: String(state.draftClaimId || '').trim(),
|
||||
currentInsight:
|
||||
state.currentInsight
|
||||
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType, currentUser.value),
|
||||
reviewFilePreviews: Array.isArray(state.reviewFilePreviews) ? state.reviewFilePreviews : [],
|
||||
composerDraft: String(state.composerDraft || ''),
|
||||
attachedFiles: [],
|
||||
composerFilesExpanded: false,
|
||||
composerUploadIntent: String(state.composerUploadIntent || '').trim(),
|
||||
insightPanelCollapsed: Boolean(state.insightPanelCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCurrentUserId() {
|
||||
const user = currentUser.value || {}
|
||||
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
||||
}
|
||||
|
||||
const initialSessionType = resolveInitialSessionType(props.initialConversation)
|
||||
const conversationInitialState = props.initialConversation
|
||||
? buildConversationSessionState(props.initialConversation, initialSessionType)
|
||||
: buildEmptySessionState(initialSessionType)
|
||||
const canRestorePersistedInitialState =
|
||||
props.entrySource === 'workbench'
|
||||
&& !String(props.initialPrompt || '').trim()
|
||||
&& !props.initialFiles.length
|
||||
const persistedInitialSnapshot = readAssistantSessionSnapshot(resolveCurrentUserId(), initialSessionType)
|
||||
const persistedInitialState = canRestorePersistedInitialState
|
||||
? buildPersistedSessionState(persistedInitialSnapshot, initialSessionType)
|
||||
: null
|
||||
const initialSessionState = canRestorePersistedInitialState && shouldPreferPersistedSessionState(
|
||||
persistedInitialState,
|
||||
persistedInitialSnapshot,
|
||||
props.initialConversation
|
||||
)
|
||||
? persistedInitialState
|
||||
: conversationInitialState
|
||||
|
||||
const activeSessionType = ref(initialSessionState.sessionType)
|
||||
const messages = ref(initialSessionState.messages)
|
||||
const conversationId = ref(initialSessionState.conversationId)
|
||||
const draftClaimId = ref(initialSessionState.draftClaimId)
|
||||
const reviewFilePreviews = ref(initialSessionState.reviewFilePreviews)
|
||||
const sessionSnapshots = ref({
|
||||
[SESSION_TYPE_EXPENSE]: null,
|
||||
[SESSION_TYPE_KNOWLEDGE]: null
|
||||
})
|
||||
const currentInsight = ref(initialSessionState.currentInsight)
|
||||
const composerUploadIntent = ref(String(initialSessionState.composerUploadIntent || '').trim())
|
||||
const insightPanelCollapsed = ref(false)
|
||||
const sessionSwitchBusy = ref(false)
|
||||
let knowledgeSessionResetPromise = Promise.resolve()
|
||||
|
||||
function buildPersistableSessionState(sessionState) {
|
||||
const state = sessionState || captureCurrentSessionState()
|
||||
return {
|
||||
sessionType: state.sessionType || SESSION_TYPE_EXPENSE,
|
||||
messages: serializeSessionMessages(state.messages),
|
||||
conversationId: String(state.conversationId || '').trim(),
|
||||
draftClaimId: String(state.draftClaimId || '').trim(),
|
||||
currentInsight: state.currentInsight || null,
|
||||
reviewFilePreviews: Array.isArray(state.reviewFilePreviews) ? state.reviewFilePreviews : [],
|
||||
composerDraft: String(state.composerDraft || ''),
|
||||
composerUploadIntent: String(state.composerUploadIntent || '').trim(),
|
||||
insightPanelCollapsed: Boolean(state.insightPanelCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
function persistSessionState(sessionState = null) {
|
||||
const state = sessionState || captureCurrentSessionState()
|
||||
const persistedState = buildPersistableSessionState(state)
|
||||
const meaningful = Boolean(
|
||||
String(persistedState.conversationId || '').trim()
|
||||
|| String(persistedState.draftClaimId || '').trim()
|
||||
|| hasMeaningfulSessionMessages(persistedState.messages)
|
||||
|| String(persistedState.composerDraft || '').trim()
|
||||
)
|
||||
|
||||
if (!meaningful) {
|
||||
clearAssistantSessionSnapshot(resolveCurrentUserId(), persistedState.sessionType)
|
||||
return
|
||||
}
|
||||
|
||||
writeAssistantSessionSnapshot(resolveCurrentUserId(), persistedState.sessionType, persistedState)
|
||||
}
|
||||
|
||||
function captureCurrentSessionState() {
|
||||
const runtimeRefs = getSessionRuntimeRefs()
|
||||
return {
|
||||
sessionType: activeSessionType.value,
|
||||
messages: messages.value,
|
||||
conversationId: conversationId.value,
|
||||
draftClaimId: draftClaimId.value,
|
||||
currentInsight: currentInsight.value,
|
||||
reviewFilePreviews: reviewFilePreviews.value,
|
||||
composerDraft: composerDraft.value,
|
||||
attachedFiles: runtimeRefs.attachedFiles?.value ?? [],
|
||||
composerFilesExpanded: runtimeRefs.composerFilesExpanded?.value ?? false,
|
||||
composerUploadIntent: composerUploadIntent.value,
|
||||
insightPanelCollapsed: insightPanelCollapsed.value
|
||||
}
|
||||
}
|
||||
|
||||
function applySessionState(sessionState) {
|
||||
const runtimeRefs = getSessionRuntimeRefs()
|
||||
const nextState = sessionState || buildEmptySessionState(activeSessionType.value)
|
||||
activeSessionType.value = nextState.sessionType || SESSION_TYPE_EXPENSE
|
||||
messages.value = Array.isArray(nextState.messages) && nextState.messages.length
|
||||
? nextState.messages
|
||||
: [
|
||||
createWelcomeAssistantMessage(
|
||||
props.entrySource,
|
||||
linkedRequest.value,
|
||||
activeSessionType.value,
|
||||
currentUser.value
|
||||
)
|
||||
]
|
||||
conversationId.value = String(nextState.conversationId || '').trim()
|
||||
draftClaimId.value = String(nextState.draftClaimId || '').trim()
|
||||
currentInsight.value =
|
||||
nextState.currentInsight
|
||||
|| buildWelcomeInsight(
|
||||
props.entrySource,
|
||||
linkedRequest.value,
|
||||
activeSessionType.value,
|
||||
currentUser.value
|
||||
)
|
||||
reviewFilePreviews.value = Array.isArray(nextState.reviewFilePreviews) ? nextState.reviewFilePreviews : []
|
||||
composerDraft.value = String(nextState.composerDraft || '')
|
||||
if (runtimeRefs.attachedFiles) {
|
||||
runtimeRefs.attachedFiles.value = Array.isArray(nextState.attachedFiles) ? nextState.attachedFiles : []
|
||||
}
|
||||
if (runtimeRefs.composerFilesExpanded) {
|
||||
runtimeRefs.composerFilesExpanded.value = Boolean(nextState.composerFilesExpanded)
|
||||
}
|
||||
composerUploadIntent.value = String(nextState.composerUploadIntent || '').trim()
|
||||
insightPanelCollapsed.value = Boolean(nextState.insightPanelCollapsed)
|
||||
uploadDecisionDialogOpen.value = false
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
async function loadLatestSessionState(targetSessionType) {
|
||||
const payload = await fetchLatestConversation(resolveCurrentUserId(), targetSessionType, {
|
||||
preferRecoverable: targetSessionType === SESSION_TYPE_EXPENSE
|
||||
})
|
||||
if (payload?.found && payload.conversation) {
|
||||
return buildConversationSessionState(payload.conversation, targetSessionType)
|
||||
}
|
||||
return buildEmptySessionState(targetSessionType)
|
||||
}
|
||||
|
||||
function resetKnowledgeSessionSnapshot() {
|
||||
const emptyKnowledgeState = buildEmptySessionState(SESSION_TYPE_KNOWLEDGE)
|
||||
sessionSnapshots.value[SESSION_TYPE_KNOWLEDGE] = emptyKnowledgeState
|
||||
|
||||
if (activeSessionType.value === SESSION_TYPE_KNOWLEDGE) {
|
||||
applySessionState(emptyKnowledgeState)
|
||||
}
|
||||
}
|
||||
|
||||
function clearKnowledgeSessionOnEntry() {
|
||||
resetKnowledgeSessionSnapshot()
|
||||
knowledgeSessionResetPromise = clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
|
||||
.catch((error) => {
|
||||
console.warn('Failed to clear knowledge session on entry:', error)
|
||||
})
|
||||
.finally(() => {
|
||||
resetKnowledgeSessionSnapshot()
|
||||
})
|
||||
return knowledgeSessionResetPromise
|
||||
}
|
||||
|
||||
async function switchSessionType(targetSessionType) {
|
||||
const normalizedTarget = String(targetSessionType || '').trim() || SESSION_TYPE_EXPENSE
|
||||
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionSnapshots.value[activeSessionType.value] = captureCurrentSessionState()
|
||||
if (sessionSnapshots.value[normalizedTarget]) {
|
||||
applySessionState(sessionSnapshots.value[normalizedTarget])
|
||||
return
|
||||
}
|
||||
|
||||
sessionSwitchBusy.value = true
|
||||
try {
|
||||
const nextState = await loadLatestSessionState(normalizedTarget)
|
||||
sessionSnapshots.value[normalizedTarget] = nextState
|
||||
applySessionState(nextState)
|
||||
} catch (error) {
|
||||
const emptyState = buildEmptySessionState(normalizedTarget)
|
||||
sessionSnapshots.value[normalizedTarget] = emptyState
|
||||
applySessionState(emptyState)
|
||||
toast(error?.message || '?????????????????')
|
||||
} finally {
|
||||
sessionSwitchBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
sessionSnapshots.value[initialSessionState.sessionType] = captureCurrentSessionState()
|
||||
|
||||
return {
|
||||
activeSessionType,
|
||||
messages,
|
||||
conversationId,
|
||||
draftClaimId,
|
||||
sessionSnapshots,
|
||||
currentInsight,
|
||||
reviewFilePreviews,
|
||||
composerUploadIntent,
|
||||
insightPanelCollapsed,
|
||||
sessionSwitchBusy,
|
||||
initialSessionState,
|
||||
buildConversationSessionState,
|
||||
buildEmptySessionState,
|
||||
buildPersistedSessionState,
|
||||
resolveCurrentUserId,
|
||||
buildPersistableSessionState,
|
||||
persistSessionState,
|
||||
captureCurrentSessionState,
|
||||
applySessionState,
|
||||
loadLatestSessionState,
|
||||
resetKnowledgeSessionSnapshot,
|
||||
clearKnowledgeSessionOnEntry,
|
||||
switchSessionType
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user