import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useSystemState } from '../useSystemState.js' import { useToast } from '../useToast.js' import { useWorkbenchComposerDate } from '../useWorkbenchComposerDate.js' import { fetchSettings } from '../../services/settings.js' import { calculateTravelReimbursement } from '../../services/reimbursements.js' import { useApplicationPreviewEditor } from '../../views/scripts/useApplicationPreviewEditor.js' import { deleteAiWorkbenchConversation, loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../../utils/aiWorkbenchConversationStore.js' import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js' import { buildAiDocumentDetailRequest, parseAiApplicationDetailHref, parseAiDocumentDetailHref } from '../../utils/aiDocumentDetailReference.js' import { AI_MODE_ACTION_ITEMS, buildSelectedFileCards, shouldRunAiAttachmentAutoAssociation } from './workbenchAiComposerModel.js' import { createWorkbenchAiMessageRuntime, formatMessageTime, normalizeInlineAttachmentOcrDetails } from './workbenchAiMessageModel.js' import { useWorkbenchAiActionRouter } from './useWorkbenchAiActionRouter.js' import { useWorkbenchAiAttachmentAssociationFlow } from './useWorkbenchAiAttachmentAssociationFlow.js' import { useWorkbenchAiApplicationPreviewFlow } from './useWorkbenchAiApplicationPreviewFlow.js' import { useWorkbenchAiComposerFiles } from './useWorkbenchAiComposerFiles.js' import { useWorkbenchAiDocumentQueryFlow } from './useWorkbenchAiDocumentQueryFlow.js' import { useWorkbenchAiExpenseFlow } from './useWorkbenchAiExpenseFlow.js' import { useWorkbenchAiMessageActions } from './useWorkbenchAiMessageActions.js' import { useWorkbenchAiSessionCommands } from './useWorkbenchAiSessionCommands.js' import { useWorkbenchAiStewardFlow } from './useWorkbenchAiStewardFlow.js' const AI_SEARCH_CONVERSATION_ID = 'ai-search' const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6 const INLINE_ANSWER_STREAM_DELAY_MS = 24 const INLINE_AUTO_SCROLL_THRESHOLD = 96 const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260 export function usePersonalWorkbenchAiMode(props, emit) { const { currentUser } = useSystemState() const { toast } = useToast() const assistantDraft = ref('') const assistantInputRef = ref(null) const fileInputRef = ref(null) const conversationScrollRef = ref(null) const inlineConversationAutoScrollPinned = ref(true) const selectedFiles = ref([]) const systemSettings = ref(null) const conversationStarted = ref(false) const conversationMessages = ref([]) const conversationId = ref('') const activeConversationTitle = ref('') const sending = ref(false) const stewardState = ref(null) const aiExpenseDraft = ref(null) const thinkingExpandedMessageIds = ref(new Set()) const thinkingCollapsedMessageIds = ref(new Set()) const attachmentOcrExpandedMessageIds = ref(new Set()) const deleteDialogOpen = ref(false) const applicationSubmitConfirmOpen = ref(false) const applicationSubmitConfirmContext = ref(null) const aiAttachmentAssociationRuntime = new Map() const messageRuntime = createWorkbenchAiMessageRuntime() const { createAiAttachmentAssociationId, createInlineMessage, normalizeRuntimeMessage, serializeRuntimeMessage } = messageRuntime const { applicationPreviewEditor, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorDateMax, resolveApplicationPreviewEditorDateMin, resolveApplicationPreviewEditorOptions, refreshApplicationPreviewEstimate, isApplicationPreviewEditing, openApplicationPreviewEditor, commitApplicationPreviewEditor, cancelApplicationPreviewEditor, handleApplicationPreviewEditorKeydown } = useApplicationPreviewEditor({ persistSessionState: () => persistCurrentConversation(), toast, calculateTravelReimbursement, currentUser }) const { workbenchDatePickerOpen, workbenchDateMode, workbenchSingleDate, workbenchRangeStartDate, workbenchRangeEndDate, workbenchDateTagLabel, workbenchCanApplyDateSelection, clearWorkbenchDateSelection, toggleWorkbenchDatePicker, closeWorkbenchDatePicker, setWorkbenchDateMode, handleWorkbenchDatePickerOutside, applyWorkbenchDateSelection, handleWorkbenchDateInputChange, removeWorkbenchDateTag, buildWorkbenchPromptText } = useWorkbenchComposerDate({ draft: assistantDraft, focusInput: focusAiModeInput }) const aiModeActionItems = AI_MODE_ACTION_ITEMS const displayUserName = computed(() => { const user = currentUser.value || {} return String(user.name || user.username || '同事').trim() || '同事' }) const displayModelName = computed(() => { const llmForm = systemSettings.value?.llmForm if (!llmForm) return 'Axiom Ultra 3.1' const model = llmForm.mainModel || '' const provider = llmForm.mainProvider || '' if (!model) return 'Axiom Ultra 3.1' return provider ? `${provider} / ${model}` : model }) const modelSelectorTitle = computed(() => { const llmForm = systemSettings.value?.llmForm if (!llmForm) return '当前模型:Axiom Ultra 3.1' const model = llmForm.mainModel || 'Axiom Ultra 3.1' const provider = llmForm.mainProvider || '' return provider ? `当前模型:${provider} / ${model}` : `当前模型:${model}` }) const filesFlow = useWorkbenchAiComposerFiles({ fileInputRef, focusAiModeInput, isInputLocked: () => isAiModeInputLocked.value, selectedFiles, toast }) const documentQueryFlow = useWorkbenchAiDocumentQueryFlow({ conversationMessages, createInlineMessage, inlineConversationAutoScrollPinned, persistCurrentConversation, replaceInlineMessage, scrollInlineConversationToBottom }) const attachmentFlow = useWorkbenchAiAttachmentAssociationFlow({ aiAttachmentAssociationRuntime, conversationMessages, createAiAttachmentAssociationId, createInlineMessage, inlineConversationAutoScrollPinned, persistCurrentConversation, replaceInlineMessage, scrollInlineConversationToBottom, sending, streamOrSetInlineAssistantContent, notifyRequestUpdated: (payload) => emit('request-updated', payload), toast }) watch(selectedFiles, (files) => { attachmentFlow.primeAiModeReceiptContext(files) }) const selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value).map((card, index) => ({ ...card, ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index]) }))) const applicationFlow = useWorkbenchAiApplicationPreviewFlow({ activateInlineConversation, applicationPreviewEditor, applicationSubmitConfirmContext, applicationSubmitConfirmOpen, assistantDraft, cancelApplicationPreviewEditor, clearAiModeFiles: filesFlow.clearAiModeFiles, closeWorkbenchDatePicker, commitApplicationPreviewEditor, conversationId, conversationMessages, conversationStarted, createInlineMessage, currentUser, handleApplicationPreviewEditorKeydown, inlineConversationAutoScrollPinned, isApplicationPreviewEditing, openApplicationPreviewEditor, persistCurrentConversation, pushInlineApplicationActionUserMessage, pushInlineUserMessage, refreshApplicationPreviewEstimate, removeWorkbenchDateTag, replaceInlineMessage, resolveApplicationPreviewEditorDateMax, resolveApplicationPreviewEditorDateMin, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, resolveInlineThinkingEvents, resolveLatestInlineUserPrompt, scrollInlineConversationToBottom, sending, toast }) const expenseFlow = useWorkbenchAiExpenseFlow({ activateInlineConversation, aiExpenseDraft, assistantDraft, clearAiModeFiles: filesFlow.clearAiModeFiles, closeWorkbenchDatePicker, conversationMessages, conversationStarted, createInlineMessage, currentUser, persistCurrentConversation, pushInlineUserMessage, replaceInlineMessage, removeWorkbenchDateTag, resolveLatestInlineUserPrompt, scrollInlineConversationToBottom, startAiApplicationPreview: applicationFlow.startAiApplicationPreview }) const actionRouter = useWorkbenchAiActionRouter({ aiExpenseDraft, applicationFlow, assistantDraft, attachmentFlow, emit, expenseFlow, focusAiModeInput, hasInlineAttachmentOcrDetails, resolveLatestInlineUserPrompt, selectedFiles, startInlineConversation, toast, toggleInlineAttachmentOcrDetails }) const sessionCommands = useWorkbenchAiSessionCommands({ activeConversationTitle, attachmentOcrExpandedMessageIds, conversationId, conversationMessages, conversationStarted, createInlineMessage, currentUser, deleteAiWorkbenchConversation, emit, focusAiModeInput, inlineConversationAutoScrollPinned, normalizeRuntimeMessage, refreshConversationHistory, resetInlineConversationState, scrollInlineConversationToBottom, stewardState, thinkingCollapsedMessageIds, thinkingExpandedMessageIds, toast }) const messageActions = useWorkbenchAiMessageActions({ assistantDraft, focusAiModeInput, persistCurrentConversation, toast }) const stewardFlow = useWorkbenchAiStewardFlow({ activeConversationTitle, collectAiModeReceiptContext: attachmentFlow.collectAiModeReceiptContext, conversationId, conversationMessages, createInlineMessage, currentUser, deleteAiWorkbenchConversation, emit, handleAiDocumentQueryIntent: documentQueryFlow.handleAiDocumentQueryIntent, inlineConversationAutoScrollPinned, persistCurrentConversation, replaceInlineMessage, resolveInlineThinkingEvents, scrollInlineConversationToBottom, sending, stewardState, streamInlineAssistantContent, updateInlineMessageContent, appendInlineMessageContent, toast }) const applicationPreviewEstimatePending = computed(() => ( conversationMessages.value.some((message) => applicationFlow.isApplicationPreviewEstimatePending(message)) )) const isAiModeInputLocked = computed(() => applicationPreviewEstimatePending.value) const canSubmitAiModePrompt = computed(() => ( !isAiModeInputLocked.value && ( Boolean(assistantDraft.value.trim()) || selectedFiles.value.length > 0 || Boolean(workbenchDateTagLabel.value) ) )) async function loadSystemSettings() { try { systemSettings.value = await fetchSettings() } catch { systemSettings.value = { llmForm: {} } } } function focusAiModeInput() { nextTick(() => { assistantInputRef.value?.focus() }) } function isInlineConversationNearBottom() { const el = conversationScrollRef.value if (!el) { return true } return el.scrollHeight - el.clientHeight - el.scrollTop <= INLINE_AUTO_SCROLL_THRESHOLD } function handleInlineConversationScroll() { inlineConversationAutoScrollPinned.value = isInlineConversationNearBottom() } function forceInlineConversationToBottom() { const el = conversationScrollRef.value if (el) { el.scrollTop = el.scrollHeight inlineConversationAutoScrollPinned.value = true } } function scrollInlineConversationToBottom(options = {}) { const shouldScroll = options.force !== false nextTick(() => { if (!shouldScroll) { return } forceInlineConversationToBottom() window.requestAnimationFrame(() => { forceInlineConversationToBottom() }) window.setTimeout(() => { if (inlineConversationAutoScrollPinned.value) { forceInlineConversationToBottom() } }, INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS) }) } function scrollInlineConversationToTop() { nextTick(() => { const el = conversationScrollRef.value if (el) { inlineConversationAutoScrollPinned.value = false el.scrollTo({ top: 0, behavior: 'smooth' }) } }) } function updateInlineMessageContent(message, content) { if (!message) { return } message.content = String(content || '') message.paragraphs = String(message.content || '') .split(/\n{2,}|\n/) .map((item) => item.trim()) .filter(Boolean) } function appendInlineMessageContent(message, delta) { const nextDelta = String(delta || '') if (!nextDelta) { return } updateInlineMessageContent(message, `${message.content || ''}${nextDelta}`) } function waitInlineAnswerStreamFrame() { return new Promise((resolve) => { window.setTimeout(resolve, INLINE_ANSWER_STREAM_DELAY_MS) }) } async function streamInlineAssistantContent(messageId, content) { const targetContent = String(content || '').trim() let streamedContent = '' for (let index = 0; index < targetContent.length; index += INLINE_ANSWER_STREAM_CHUNK_SIZE) { const message = conversationMessages.value.find((item) => item.id === messageId) if (!message || !message.pending) { return } const shouldAutoScroll = inlineConversationAutoScrollPinned.value streamedContent += targetContent.slice(index, index + INLINE_ANSWER_STREAM_CHUNK_SIZE) updateInlineMessageContent(message, streamedContent) scrollInlineConversationToBottom({ force: shouldAutoScroll }) await waitInlineAnswerStreamFrame() } } async function streamOrSetInlineAssistantContent(messageId, content) { const targetContent = String(content || '').trim() if (//.test(targetContent)) { const message = conversationMessages.value.find((item) => item.id === messageId) if (message?.pending) { updateInlineMessageContent(message, targetContent) scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) } return } await streamInlineAssistantContent(messageId, targetContent) } function refreshConversationHistory() { const history = loadAiWorkbenchConversationHistory(currentUser.value || {}) emit('conversation-history-change', history) return history } function isPersistableInlineConversation() { return Boolean( conversationId.value && conversationId.value !== AI_SEARCH_CONVERSATION_ID && conversationMessages.value.length ) } function persistCurrentConversation() { if (!isPersistableInlineConversation()) { refreshConversationHistory() return [] } const history = saveAiWorkbenchConversation(currentUser.value || {}, { id: conversationId.value, conversationId: conversationId.value, title: activeConversationTitle.value, source: 'workbench', sessionType: 'steward', stewardState: stewardState.value, messages: conversationMessages.value.map((message) => serializeRuntimeMessage(message)) }) emit('conversation-history-change', history) return history } function resetInlineConversationState() { conversationStarted.value = false conversationMessages.value = [] conversationId.value = '' stewardState.value = null activeConversationTitle.value = '' assistantDraft.value = '' thinkingExpandedMessageIds.value = new Set() thinkingCollapsedMessageIds.value = new Set() attachmentOcrExpandedMessageIds.value = new Set() deleteDialogOpen.value = false applicationSubmitConfirmOpen.value = false applicationSubmitConfirmContext.value = null clearWorkbenchDateSelection() filesFlow.clearAiModeFiles() } function replaceInlineMessage(id, nextMessage) { const index = conversationMessages.value.findIndex((item) => item.id === id) if (index === -1) { conversationMessages.value.push(nextMessage) return } conversationMessages.value.splice(index, 1, nextMessage) } function activateInlineConversation(options = {}) { conversationStarted.value = true if (!conversationId.value) { conversationId.value = options.id || `inline-${Date.now()}` } activeConversationTitle.value = options.title || activeConversationTitle.value || '新对话' emit('conversation-change', { id: conversationId.value, title: activeConversationTitle.value }) } function renderInlineConversationHtml(content) { return renderAiConversationHtml(content) } function resolveInlineThinkingEvents(message) { return Array.isArray(message?.stewardPlan?.thinkingEvents) ? message.stewardPlan.thinkingEvents : [] } function hasInlineThinking(message) { return resolveInlineThinkingEvents(message).length > 0 } function isInlineThinkingExpanded(message) { if (!message?.id) { return Boolean(message?.pending) } if (thinkingCollapsedMessageIds.value.has(message.id)) { return false } return Boolean(message.pending || thinkingExpandedMessageIds.value.has(message.id)) } function toggleInlineThinking(message) { if (!message?.id) { return } const nextExpandedIds = new Set(thinkingExpandedMessageIds.value) const nextCollapsedIds = new Set(thinkingCollapsedMessageIds.value) if (isInlineThinkingExpanded(message)) { nextExpandedIds.delete(message.id) nextCollapsedIds.add(message.id) } else { nextCollapsedIds.delete(message.id) nextExpandedIds.add(message.id) } thinkingExpandedMessageIds.value = nextExpandedIds thinkingCollapsedMessageIds.value = nextCollapsedIds } function hasInlineAttachmentOcrDetails(message = {}) { const details = normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null) return Boolean(details?.documents?.length || details?.fileNames?.length) } function resolveInlineAttachmentOcrDocuments(message = {}) { return normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)?.documents || [] } function resolveInlineAttachmentOcrFileCount(message = {}) { const details = normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null) return Math.max(details?.documents?.length || 0, details?.fileNames?.length || 0) } function isInlineAttachmentOcrExpanded(message = {}) { return Boolean(message?.id && attachmentOcrExpandedMessageIds.value.has(message.id)) } function toggleInlineAttachmentOcrDetails(message = {}, forceExpanded = null) { if (!message?.id || !hasInlineAttachmentOcrDetails(message)) { return } const nextExpandedIds = new Set(attachmentOcrExpandedMessageIds.value) const shouldExpand = forceExpanded === null ? !nextExpandedIds.has(message.id) : Boolean(forceExpanded) if (shouldExpand) { nextExpandedIds.add(message.id) } else { nextExpandedIds.delete(message.id) } attachmentOcrExpandedMessageIds.value = nextExpandedIds nextTick(() => { scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) }) } function buildInlinePromptText(rawPrompt, files = []) { const prompt = buildWorkbenchPromptText(rawPrompt) if (prompt) { return prompt } return files.length ? '请帮我处理已上传的附件。' : '' } function isReimbursementCreationIntent(prompt = '') { const compact = String(prompt || '').replace(/\s+/g, '') if (!compact || !/报销|报账/.test(compact)) { return false } if (/标准|制度|规则|政策|口径|怎么|如何|能不能|可以吗|查一下|查询|状态|进度|多少钱|多少/.test(compact)) { return false } return ( /^(我要|我想|我需要|帮我|请帮我|需要|发起|新建|创建|提交|办理|走).{0,16}(报销|报账)/.test(compact) || /^(报销|报账)(一下|一笔|单|流程)?$/.test(compact) ) } function handleAiAnswerMarkdownClick(event) { const target = event?.target const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]') if (!link) { return } const href = link.getAttribute('href') const detailReference = parseAiDocumentDetailHref(href) || parseAiApplicationDetailHref(href) if (!detailReference) { return } event.preventDefault() event.stopPropagation() emit('open-document', buildAiDocumentDetailRequest(detailReference)) } function startInlineConversation(prompt, entry = {}, files = []) { if (isAiModeInputLocked.value) { toast('请等待费用测算完成后再继续操作。') return } const cleanPrompt = buildInlinePromptText(prompt, files) if (!cleanPrompt || sending.value) { return } if (aiExpenseDraft.value && !isAiExpenseDraftComplete(aiExpenseDraft.value)) { expenseFlow.advanceAiExpenseDraft(cleanPrompt, files) return } if (applicationFlow.handleInlineApplicationPreviewTextAction(cleanPrompt, applicationPreviewEstimatePending)) { return } if (isReimbursementCreationIntent(cleanPrompt)) { void expenseFlow.startAiReimbursementAssociationGate(cleanPrompt, entry.label || '我要报销') return } if (conversationId.value === AI_SEARCH_CONVERSATION_ID) { conversationId.value = '' conversationMessages.value = [] activeConversationTitle.value = '' } sending.value = true activateInlineConversation({ title: entry.label || cleanPrompt.slice(0, 18) || '新对话' }) inlineConversationAutoScrollPinned.value = true conversationMessages.value.push(createInlineMessage('user', cleanPrompt)) assistantDraft.value = '' removeWorkbenchDateTag() closeWorkbenchDatePicker() filesFlow.clearAiModeFiles() scrollInlineConversationToBottom() persistCurrentConversation() if (shouldRunAiAttachmentAutoAssociation(entry, files, cleanPrompt)) { void attachmentFlow.requestAiAttachmentAssociationReply(cleanPrompt, entry, files) return } void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files) } function submitAiModePrompt() { if (!canSubmitAiModePrompt.value) { toast('请输入需求后再发送。') focusAiModeInput() return } startInlineConversation(assistantDraft.value, { source: 'workbench', sessionType: 'steward' }, Array.from(selectedFiles.value)) } function runAiModeAction(item) { if (String(item?.label || '').trim() === '发起报销') { void expenseFlow.startAiReimbursementAssociationGate(item.prompt, item.label) return } startInlineConversation(item.prompt, item, Array.from(selectedFiles.value)) } function regenerateLastReply() { const lastUserMessage = [...conversationMessages.value].reverse().find((message) => message.role === 'user') if (!lastUserMessage || sending.value) { return } const lastAssistantIndex = conversationMessages.value.map((message) => message.role).lastIndexOf('assistant') if (lastAssistantIndex >= 0) { conversationMessages.value.splice(lastAssistantIndex, 1) } sending.value = true void stewardFlow.requestInlineAssistantReply(lastUserMessage.content, { source: 'workbench', sessionType: 'steward' }, []) } function pushInlineUserMessage(text) { conversationMessages.value.push(createInlineMessage('user', String(text || '').trim())) } function pushInlineApplicationActionUserMessage(text) { pushInlineUserMessage(text) assistantDraft.value = '' removeWorkbenchDateTag() closeWorkbenchDatePicker() filesFlow.clearAiModeFiles() } function resolveLatestInlineUserPrompt() { const latestUserMessage = [...conversationMessages.value].reverse().find((message) => message.role === 'user') return String(latestUserMessage?.content || '').trim() } function handleVoiceInput() { if (isAiModeInputLocked.value) { toast('请等待费用测算完成后再继续操作。') return } toast('语音输入正在准备中,您可以先输入文字需求。') focusAiModeInput() } watch( () => props.sidebarCommand?.seq, () => { const command = props.sidebarCommand || {} if (command.type === 'new-chat') { sessionCommands.startNewInlineConversation() return } if (command.type === 'search-chat') { sessionCommands.openInlineSearchConversation(activateInlineConversation) return } if (command.type === 'open-recent') { sessionCommands.openInlineRecentConversation(command.payload || {}) } } ) onMounted(() => { loadSystemSettings() refreshConversationHistory() document.addEventListener('click', handleWorkbenchDatePickerOutside) }) onBeforeUnmount(() => { document.removeEventListener('click', handleWorkbenchDatePickerOutside) }) return { activeConversationTitle, aiModeActionItems, applicationPreviewEditor, applicationSubmitConfirmOpen, assistantInputRef, assistantDraft, canShowInlineSuggestedActions: applicationFlow.canShowInlineSuggestedActions, canSubmitAiModePrompt, cancelDeleteConversation: () => sessionCommands.cancelDeleteConversation(deleteDialogOpen), cancelInlineApplicationSubmitConfirm: applicationFlow.cancelInlineApplicationSubmitConfirm, clearWorkbenchDateSelection, commitInlineApplicationPreviewEditor: applicationFlow.commitInlineApplicationPreviewEditor, confirmDeleteConversation: () => sessionCommands.confirmDeleteConversation(deleteDialogOpen), confirmInlineApplicationSubmit: applicationFlow.confirmInlineApplicationSubmit, conversationMessages, conversationScrollRef, conversationStarted, deleteDialogOpen, displayModelName, displayUserName, fileInputRef, handleAiAnswerMarkdownClick, handleAiModeFilesChange: filesFlow.handleAiModeFilesChange, handleInlineApplicationPreviewEditorKeydown: applicationFlow.handleInlineApplicationPreviewEditorKeydown, handleInlineConversationScroll, handleInlineSuggestedAction: actionRouter.handleInlineSuggestedAction, handleVoiceInput, hasInlineAttachmentOcrDetails, hasInlineThinking, isAiModeInputLocked, isApplicationPreviewEditing: applicationFlow.isApplicationPreviewEditing, isApplicationPreviewEstimatePending: applicationFlow.isApplicationPreviewEstimatePending, isInlineAttachmentOcrExpanded, isInlineSuggestedActionDisabled: applicationFlow.isInlineSuggestedActionDisabled, isInlineThinkingExpanded, markInlineMessageFeedback: messageActions.markInlineMessageFeedback, modelSelectorTitle, openApplicationPreviewEditor: applicationFlow.openApplicationPreviewEditor, quoteInlineMessage: messageActions.quoteInlineMessage, regenerateLastReply, removeAiModeFile: filesFlow.removeAiModeFile, removeWorkbenchDateTag, renderInlineConversationHtml, requestDeleteCurrentConversation: () => sessionCommands.requestDeleteCurrentConversation(deleteDialogOpen), resolveApplicationPreviewEditorOptions: applicationFlow.resolveApplicationPreviewEditorOptions, resolveInlineApplicationPreviewEditorControl: applicationFlow.resolveInlineApplicationPreviewEditorControl, resolveInlineApplicationPreviewEditorDateMax: applicationFlow.resolveInlineApplicationPreviewEditorDateMax, resolveInlineApplicationPreviewEditorDateMin: applicationFlow.resolveInlineApplicationPreviewEditorDateMin, resolveInlineApplicationPreviewMissingFields: applicationFlow.resolveInlineApplicationPreviewMissingFields, resolveInlineApplicationPreviewRows: applicationFlow.resolveInlineApplicationPreviewRows, resolveInlineAttachmentOcrDocuments, resolveInlineAttachmentOcrFileCount, resolveInlineThinkingEvents, runAiModeAction, scrollInlineConversationToTop, selectedFileCards, sending, setWorkbenchDateMode, submitAiModePrompt, toggleInlineAttachmentOcrDetails, toggleInlineThinking, toggleWorkbenchDatePicker, triggerAiModeFileUpload: filesFlow.triggerAiModeFileUpload, workbenchCanApplyDateSelection, workbenchDateMode, workbenchDatePickerOpen, workbenchDateTagLabel, workbenchRangeEndDate, workbenchRangeStartDate, workbenchSingleDate, applyWorkbenchDateSelection, buildInlineApplicationPreviewFooterText: applicationFlow.buildInlineApplicationPreviewFooterText, copyInlineMessage: messageActions.copyInlineMessage, formatMessageTime, handleWorkbenchDateInputChange } }