Files
X-Financial/web/src/views/scripts/useTravelReimbursementSessionState.js

336 lines
12 KiB
JavaScript
Raw Normal View History

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
}
}