Files
X-Financial/web/src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js

769 lines
27 KiB
JavaScript
Raw Normal View History

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