import { nextTick, ref } from 'vue' import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js' import { clearAssistantSessionSnapshot, readAssistantSessionSnapshot, writeAssistantSessionSnapshot } from '../../utils/assistantSessionSnapshot.js' import { buildReviewFilePreviewsFromMessages, filterPersistableFilePreviews } 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, 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: filterPersistableFilePreviews(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 shouldPersistLocalSnapshot = props.entrySource !== 'detail' 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: filterPersistableFilePreviews(state.reviewFilePreviews), composerDraft: String(state.composerDraft || ''), composerUploadIntent: String(state.composerUploadIntent || '').trim(), insightPanelCollapsed: Boolean(state.insightPanelCollapsed) } } function persistSessionState(sessionState = null) { if (!shouldPersistLocalSnapshot) { return } 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) 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 } }