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

823 lines
28 KiB
JavaScript
Raw Normal View History

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 (/<!--\s*ai-trusted-html:start\s*-->/.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
}
}