Files
X-Financial/web/src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js
2026-06-22 11:58:53 +08:00

788 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, 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,
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 selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value))
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,
toast
})
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,
resolveApplicationPreviewEditorControl,
resolveApplicationPreviewEditorOptions,
resolveInlineThinkingEvents,
resolveLatestInlineUserPrompt,
scrollInlineConversationToBottom,
sending,
toast
})
const expenseFlow = useWorkbenchAiExpenseFlow({
activateInlineConversation,
aiExpenseDraft,
assistantDraft,
clearAiModeFiles: filesFlow.clearAiModeFiles,
closeWorkbenchDatePicker,
conversationMessages,
conversationStarted,
createInlineMessage,
currentUser,
persistCurrentConversation,
pushInlineUserMessage,
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 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 (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() === '发起报销') {
expenseFlow.pushInlineExpenseSceneSelectionPrompt(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,
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
}
}