refactor: enforce 800 line source limits
This commit is contained in:
@@ -0,0 +1,787 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user