Files
X-Financial/web/src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js
caoxiaozhu 787bc3a481 feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
2026-07-17 14:14:08 +08:00

769 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed, 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, fetchExpenseClaimDetail } from '../../services/reimbursements.js'
import { useApplicationPreviewEditor } from '../../views/scripts/useApplicationPreviewEditor.js'
import {
deleteAiWorkbenchConversation,
markAiWorkbenchConversationDocumentDeleted
} from '../../utils/aiWorkbenchConversationStore.js'
import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js'
import {
buildAiDocumentDetailRequest,
parseAiApplicationDetailHref,
parseAiDocumentDetailHref
} from '../../utils/aiDocumentDetailReference.js'
import {
AI_MODE_ACTION_ITEMS,
shouldRunAiAttachmentAutoAssociation
} from './workbenchAiComposerModel.js'
import {
createWorkbenchAiMessageRuntime,
formatMessageTime
} 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 { useWorkbenchAiFilePreview } from './useWorkbenchAiFilePreview.js'
import { useWorkbenchAiMessageActions } from './useWorkbenchAiMessageActions.js'
import { useWorkbenchAiMessageExpansion } from './useWorkbenchAiMessageExpansion.js'
import { useWorkbenchAiSessionCommands } from './useWorkbenchAiSessionCommands.js'
import { useWorkbenchAiStewardFlow } from './useWorkbenchAiStewardFlow.js'
import {
isReimbursementCreationIntent
} from './workbenchAiApplicationGateModel.js'
import { useWorkbenchAiCommandIntents } from './useWorkbenchAiCommandIntents.js'
import {
shouldRequestWorkbenchAiIntentPlan
} from './workbenchAiIntentPlannerModel.js'
import { useWorkbenchAiConversationRuntime } from './useWorkbenchAiConversationRuntime.js'
import { useWorkbenchAiIntentExecution } from './useWorkbenchAiIntentExecution.js'
const AI_SEARCH_CONVERSATION_ID = 'ai-search'
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 conversationRuntime = useWorkbenchAiConversationRuntime({
activeConversationTitle,
applicationSubmitConfirmContext,
applicationSubmitConfirmOpen,
assistantDraft,
assistantInputRef,
attachmentOcrExpandedMessageIds,
clearAiModeFiles: () => filesFlow.clearAiModeFiles(),
clearWorkbenchDateSelection,
conversationId,
conversationMessages,
conversationScrollRef,
conversationStarted,
currentUser,
deleteDialogOpen,
emit,
inlineConversationAutoScrollPinned,
searchConversationId: AI_SEARCH_CONVERSATION_ID,
serializeRuntimeMessage,
stewardState,
thinkingCollapsedMessageIds,
thinkingExpandedMessageIds
})
const {
activateInlineConversation,
appendInlineMessageContent,
focusAiModeInput,
handleInlineConversationScroll,
persistCurrentConversation,
refreshConversationHistory,
replaceInlineMessage,
resetInlineConversationState,
scrollInlineConversationToBottom,
scrollInlineConversationToTop,
setAssistantInputRef,
streamInlineAssistantContent,
streamOrSetInlineAssistantContent,
updateInlineMessageContent
} = conversationRuntime
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,
resolveInputLockedMessage: () => resolveAiModeInputLockMessage(),
selectedFiles,
toast
})
const documentQueryFlow = useWorkbenchAiDocumentQueryFlow({
conversationMessages,
createInlineMessage,
inlineConversationAutoScrollPinned,
persistCurrentConversation,
replaceInlineMessage,
scrollInlineConversationToBottom
})
const commandIntents = useWorkbenchAiCommandIntents({
activateInlineConversation,
activeConversationTitle,
assistantDraft,
clearAiModeFiles: filesFlow.clearAiModeFiles,
closeWorkbenchDatePicker,
conversationId,
conversationMessages,
createInlineMessage,
documentQueryFlow,
inlineConversationAutoScrollPinned,
persistCurrentConversation,
removeWorkbenchDateTag,
scrollInlineConversationToBottom,
searchConversationId: AI_SEARCH_CONVERSATION_ID,
sending
})
const attachmentFlow = useWorkbenchAiAttachmentAssociationFlow({
aiAttachmentAssociationRuntime,
conversationId,
conversationMessages,
createAiAttachmentAssociationId,
createInlineMessage,
inlineConversationAutoScrollPinned,
persistCurrentConversation,
replaceInlineMessage,
scrollInlineConversationToBottom,
sending,
streamOrSetInlineAssistantContent,
notifyRequestUpdated: (payload) => emit('request-updated', payload),
toast
})
const filePreview = useWorkbenchAiFilePreview({
attachmentFlow,
conversationStarted,
scrollInlineConversationToBottom,
selectedFiles
})
const {
hasInlineAttachmentOcrDetails,
hasInlineThinking,
isInlineAttachmentOcrExpanded,
isInlineThinkingExpanded,
resolveInlineAttachmentOcrDocuments,
resolveInlineAttachmentOcrFileCount,
resolveInlineThinkingEvents,
toggleInlineAttachmentOcrDetails,
toggleInlineThinking
} = useWorkbenchAiMessageExpansion({
attachmentOcrExpandedMessageIds,
inlineConversationAutoScrollPinned,
scrollInlineConversationToBottom,
thinkingCollapsedMessageIds,
thinkingExpandedMessageIds
})
let intentExecution = null
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,
onApplicationActionCompleted: (remainingTasks) => intentExecution?.startModelPlannedNextTask(remainingTasks)
})
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,
conversationMessages,
createInlineMessage,
emit,
expenseFlow,
focusAiModeInput,
hasInlineAttachmentOcrDetails,
persistCurrentConversation,
replaceInlineMessage,
resolveLatestInlineUserPrompt,
scrollInlineConversationToBottom,
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
})
intentExecution = useWorkbenchAiIntentExecution({
actionRouter,
activeConversationTitle,
activateInlineConversation,
applicationFlow,
assistantDraft,
clearAiModeFiles: filesFlow.clearAiModeFiles,
closeWorkbenchDatePicker,
conversationId,
conversationMessages,
createInlineMessage,
expenseFlow,
inlineConversationAutoScrollPinned,
persistCurrentConversation,
removeWorkbenchDateTag,
replaceInlineMessage,
resolveInlineThinkingEvents,
scrollInlineConversationToBottom,
searchConversationId: AI_SEARCH_CONVERSATION_ID,
sending,
stewardFlow
})
const applicationPreviewEstimatePending = computed(() => (
conversationMessages.value.some((message) => applicationFlow.isApplicationPreviewEstimatePending(message))
))
const isAiModeReceiptRecognitionPending = computed(() => attachmentFlow.hasPendingAiModeReceiptRecognition(selectedFiles.value))
const hasAiModeReceiptRecognitionFailure = computed(() => attachmentFlow.hasFailedAiModeReceiptRecognition(selectedFiles.value))
const isAiModeInputLocked = computed(() => applicationPreviewEstimatePending.value || isAiModeReceiptRecognitionPending.value)
const aiModeInputLockMessage = computed(() => resolveAiModeInputLockMessage())
const canSubmitAiModePrompt = computed(() => (
!isAiModeInputLocked.value &&
!hasAiModeReceiptRecognitionFailure.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 renderInlineConversationHtml(content) { return renderAiConversationHtml(content) }
function isUnavailableDocumentDetailError(error) {
const message = String(error?.message || '').trim()
return /not\s*found|不存在|已删除|不可访问|无权|forbidden|404/i.test(message)
}
function resolveDocumentDetailLookupClaimId(detailRequest = {}) {
return String(detailRequest.claimId || detailRequest.claim_id || '').trim()
}
function markActiveAiDocumentDetailLinkDeleted(detailRequest = {}) {
const nextHistory = markAiWorkbenchConversationDocumentDeleted(currentUser.value || {}, {
claimId: detailRequest.claimId,
claim_id: detailRequest.claimId,
claimNo: detailRequest.claimNo,
claim_no: detailRequest.claimNo,
documentNo: detailRequest.documentNo,
document_no: detailRequest.documentNo,
id: detailRequest.id
})
emit('conversation-history-change', nextHistory)
const activeConversation = nextHistory.find((item) => (
String(item.id || item.conversationId || '').trim() === String(conversationId.value || '').trim()
))
if (activeConversation?.messages?.length) {
conversationMessages.value = activeConversation.messages.map((message) => normalizeRuntimeMessage(message))
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
}
}
async function ensureAiDocumentDetailStillAvailable(detailRequest = {}) {
const claimId = resolveDocumentDetailLookupClaimId(detailRequest)
if (!claimId) {
return true
}
try {
await fetchExpenseClaimDetail(claimId)
return true
} catch (error) {
if (!isUnavailableDocumentDetailError(error)) {
console.warn('AI document detail availability check failed, continuing navigation:', error)
return true
}
markActiveAiDocumentDetailLinkDeleted(detailRequest)
toast('该单据已经删除或不可访问,已将这条历史入口标记为不可查看。')
return false
}
}
function buildInlinePromptText(rawPrompt, files = []) {
const prompt = buildWorkbenchPromptText(rawPrompt)
if (prompt) return prompt
return files.length ? '请帮我处理已上传的附件。' : ''
}
function resolveAiModeInputLockMessage() {
if (isAiModeReceiptRecognitionPending.value) {
return '附件识别中,请稍等...'
}
if (applicationPreviewEstimatePending.value) {
return '费用测算中,请稍等...'
}
return ''
}
function resolveAiModeSubmitBlockedMessage() {
if (applicationPreviewEstimatePending.value) {
return '请等待费用测算完成后再继续操作。'
}
if (isAiModeReceiptRecognitionPending.value) {
return '附件 OCR 识别中,请稍等,识别完成后再继续对话。'
}
if (hasAiModeReceiptRecognitionFailure.value) {
return '请先移除识别失败的附件或重新上传。'
}
return ''
}
function ensureAiModeCanStartConversation() {
const blockedMessage = resolveAiModeSubmitBlockedMessage()
if (!blockedMessage) {
return true
}
toast(blockedMessage)
focusAiModeInput()
return false
}
async 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()
const detailRequest = buildAiDocumentDetailRequest(detailReference)
if (!(await ensureAiDocumentDetailStillAvailable(detailRequest))) {
return
}
emit('open-document', detailRequest)
}
function startInlineConversation(prompt, entry = {}, files = []) {
if (!ensureAiModeCanStartConversation()) {
return
}
const cleanPrompt = buildInlinePromptText(prompt, files)
if (!cleanPrompt || sending.value) {
return
}
if (commandIntents.handleInlineDraftDeletionIntent(cleanPrompt, entry)) {
return
}
if (applicationFlow.handleInlineApplicationPreviewTextAction(cleanPrompt, applicationPreviewEstimatePending)) {
return
}
if (aiExpenseDraft.value && !isAiExpenseDraftComplete(aiExpenseDraft.value)) {
expenseFlow.advanceAiExpenseDraft(cleanPrompt, files)
return
}
if (shouldRequestWorkbenchAiIntentPlan(cleanPrompt)) {
void intentExecution.executeModelPlannedWorkbenchIntent(cleanPrompt, entry, files)
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 (!ensureAiModeCanStartConversation()) {
return
}
if (!canSubmitAiModePrompt.value) {
toast('请输入需求后再发送。')
focusAiModeInput()
return
}
startInlineConversation(assistantDraft.value, { source: 'workbench', sessionType: 'steward' }, Array.from(selectedFiles.value))
}
function runAiModeAction(item) {
if (!ensureAiModeCanStartConversation()) {
return
}
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() {
return String([...conversationMessages.value].reverse().find((message) => message.role === 'user')?.content || '').trim()
}
function handleVoiceInput() {
if (!ensureAiModeCanStartConversation()) {
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 || {})
attachmentFlow.resumePendingAiAttachmentAssociationJobs()
expenseFlow.resumePendingLinkedReimbursementDraftJobs()
}
}
)
onMounted(() => {
loadSystemSettings()
refreshConversationHistory()
attachmentFlow.resumePendingAiAttachmentAssociationJobs()
expenseFlow.resumePendingLinkedReimbursementDraftJobs()
document.addEventListener('click', handleWorkbenchDatePickerOutside)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleWorkbenchDatePickerOutside)
})
return {
activeConversationTitle,
aiModeActionItems,
aiModeInputLockMessage,
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,
...filePreview,
sending,
setAssistantInputRef,
setWorkbenchDateMode,
submitAiModePrompt,
toggleInlineAttachmentOcrDetails,
toggleInlineThinking,
toggleWorkbenchDatePicker,
triggerAiModeFileUpload: filesFlow.triggerAiModeFileUpload,
workbenchCanApplyDateSelection,
workbenchDateMode,
workbenchDatePickerOpen,
workbenchDateTagLabel,
workbenchRangeEndDate,
workbenchRangeStartDate,
workbenchSingleDate,
applyWorkbenchDateSelection,
buildInlineApplicationPreviewFooterText: applicationFlow.buildInlineApplicationPreviewFooterText,
copyInlineMessage: messageActions.copyInlineMessage,
formatMessageTime,
handleWorkbenchDateInputChange
}
}