336 lines
12 KiB
JavaScript
336 lines
12 KiB
JavaScript
|
|
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
|
||
|
|
}
|
||
|
|
}
|