From 9a2af0a4120547ce7bb3e9410b88034a898034ad Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 3 Jul 2026 13:19:30 +0800 Subject: [PATCH] feat: add agent pet types and state management for idle actions - Introduced new types for agent pet actions and intents in `types.ts`. - Implemented `useAgentPetMachine` to manage idle action scheduling and execution. - Added functionality to handle random actions based on interaction states. --- src/components/agent/AgentAssistantEntry.vue | 1640 ++--------------- src/components/agent/AgentAssistantWidget.vue | 2 + src/components/agent/pet/AgentPetStage.vue | 28 + src/components/agent/pet/agentPetActions.ts | 77 + .../agent/pet/renderers/CssRobotRenderer.vue | 44 + .../agent/pet/renderers/cssRobot.scss | 1402 ++++++++++++++ src/components/agent/pet/types.ts | 27 + .../agent/pet/useAgentPetMachine.ts | 127 ++ 8 files changed, 1817 insertions(+), 1530 deletions(-) create mode 100644 src/components/agent/pet/AgentPetStage.vue create mode 100644 src/components/agent/pet/agentPetActions.ts create mode 100644 src/components/agent/pet/renderers/CssRobotRenderer.vue create mode 100644 src/components/agent/pet/renderers/cssRobot.scss create mode 100644 src/components/agent/pet/types.ts create mode 100644 src/components/agent/pet/useAgentPetMachine.ts diff --git a/src/components/agent/AgentAssistantEntry.vue b/src/components/agent/AgentAssistantEntry.vue index 13d26d92..2b6deacc 100644 --- a/src/components/agent/AgentAssistantEntry.vue +++ b/src/components/agent/AgentAssistantEntry.vue @@ -8,6 +8,9 @@ import { type AgentAssistantNotificationBubblePayload, } from '@/utils/agentAssistantBubble' import { useI18n } from 'vue-i18n' +import AgentPetStage from './pet/AgentPetStage.vue' +import type { AgentPetActionName, AgentPetIntent } from './pet/types' +import { useAgentPetMachine } from './pet/useAgentPetMachine' interface AgentAssistantEntryBubble { id: string @@ -60,25 +63,11 @@ const FAB_BUBBLE_SAFE_MARGIN = 12 const FAB_BUBBLE_ARROW_MARGIN = 28 const FAB_BUBBLE_EDGE_ARROW_OFFSET = 38 const FAB_BUBBLE_UNDOCK_POSITION_SYNC_DELAY = 260 -const FAB_RANDOM_ACTION_MIN_DELAY = 8000 -const FAB_RANDOM_ACTION_MAX_DELAY = 18000 const FAB_RIGHT_EDGE_RESIZE_FOLLOW_DISTANCE = 128 const FAB_DRAG_SUPPRESS_CLICK_DELAY = 450 -const FAB_RANDOM_ACTIONS = ['wave', 'sit', 'eye-roll', 'faint', 'disassemble', 'happy-jump'] as const - -type FabRandomAction = (typeof FAB_RANDOM_ACTIONS)[number] type FabBubblePlacement = 'bottom' | 'left' | 'right' | 'top' -const FAB_RANDOM_ACTION_DURATIONS: Record = { - wave: 2450, - sit: 4200, - 'eye-roll': 1900, - faint: 4800, - disassemble: 6200, - 'happy-jump': 5200, -} - // 入口位置只保存在当前页面生命周期内,刷新后回到默认位置。 interface FabPosition { x: number @@ -188,7 +177,6 @@ const fabBubbleArrowSource = ref({ const fabPressed = ref(false) const fabBubbles = ref([]) const fabDragging = ref(false) -const fabRandomAction = ref(null) let fabIdleTimer: number | null = null let fabDragState: FabDragState | null = null @@ -199,9 +187,6 @@ let fabPendingPointerPoint: FabPointerPoint | null = null let fabBubblePositionFrame = 0 let fabBubbleResizeObserver: ResizeObserver | null = null let fabBubbleUndockPositionTimer: number | null = null -let fabLastRandomAction: FabRandomAction | null = null -let fabRandomActionTimer: number | null = null -let fabRandomActionEndTimer: number | null = null let stopBubbleListener: (() => void) | null = null let fabPositionAnchor: FabPositionAnchor | null = null let stopFabTouchMoveGuard: (() => void) | null = null @@ -215,11 +200,35 @@ const fabBubbleClassList = computed(() => [ `agent-assistant-fab__bubbles--arrow-${fabBubbleArrowSource.value.kind}`, `agent-assistant-fab__bubbles--arrow-${fabBubbleArrowSource.value.variant}`, ]) +const agentPetIntent = computed(() => { + if (fabDragging.value) return 'dragging' + if (fabDocked.value) return 'docked' + if (props.thinking) return 'thinking' + if (hasFabBubbles.value) return 'notify' + return 'idle' +}) +const { + clearAction: clearFabRandomAction, + currentAction: fabRandomAction, + playAction: playAgentPetAction, + scheduleRandomAction: scheduleFabRandomAction, +} = useAgentPetMachine({ + active: () => props.active, + docked: fabDocked, + dragging: fabDragging, + pressed: fabPressed, + scheduleAutoDock: scheduleFabAutoDock, + shouldAutoDock: shouldFabAutoDock, + thinking: () => props.thinking, +}) + +// 生成气泡唯一 ID,避免通知、toast 和预览气泡在堆叠中冲突。 function createBubbleId(prefix = 'bubble') { return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}` } +// 获取当前可用视口尺寸,兼容布局视口和可视视口短暂不同步的场景。 function getViewportSize() { const layoutWidth = window.innerWidth || document.documentElement.clientWidth || 0 const layoutHeight = window.innerHeight || document.documentElement.clientHeight || 0 @@ -244,6 +253,7 @@ function getFabBubbleGap() { return isMobileFabViewport() ? FAB_MOBILE_BUBBLE_GAP : FAB_BUBBLE_GAP } +// 获取展开状态下入口外层容器尺寸,用于默认定位和拖拽边界计算。 function getOpenFabSize() { const viewport = getViewportSize() const isMobile = isMobileFabViewport() @@ -254,6 +264,7 @@ function getOpenFabSize() { } } +// 在 DOM 尺寸不可用时返回入口触发热区的兜底边界。 function getFallbackFabInteractiveBounds(): FabInteractiveBounds { const rootSize = getOpenFabSize() const triggerSize = isMobileFabViewport() ? { height: 77, width: 80 } : { height: 82, width: 86 } @@ -268,10 +279,12 @@ function getFallbackFabInteractiveBounds(): FabInteractiveBounds { } } +// 计算入口贴到右侧边缘时的横向坐标。 function getDockedFabX() { return Math.max(0, getViewportSize().width - 42) } +// 获取实际机器人触发热区边界,避免外层空白影响拖拽贴边。 function getFabInteractiveBounds(): FabInteractiveBounds { const root = getFabRootElement() const trigger = root?.querySelector('.agent-assistant-fab__trigger') as HTMLElement | null @@ -293,6 +306,7 @@ function getFabInteractiveBounds(): FabInteractiveBounds { return getFallbackFabInteractiveBounds() } +// 计算入口默认落点,刷新后回到右侧约三分之二高度。 function getDefaultFabPosition() { if (typeof window === 'undefined') return { x: 0, y: 0 } @@ -306,6 +320,7 @@ function getDefaultFabPosition() { }) } +// 计算入口在当前视口内允许移动的坐标范围。 function getFabPositionRange(options: FabPositionRangeOptions = {}) { const viewport = getViewportSize() const bounds = options.useOpenBounds ? getFallbackFabInteractiveBounds() : getFabInteractiveBounds() @@ -317,6 +332,7 @@ function getFabPositionRange(options: FabPositionRangeOptions = {}) { return { maxX, maxY, minX, minY } } +// 把入口位置限制在允许移动范围内。 function clampFabPosition(position: FabPosition, options: FabPositionRangeOptions = {}) { if (typeof window === 'undefined') return position @@ -328,16 +344,19 @@ function clampFabPosition(position: FabPosition, options: FabPositionRangeOption } } +// 把入口纵向位置限制在允许移动范围内。 function clampFabY(y: number, options: FabPositionRangeOptions = {}) { const range = getFabPositionRange(options) return Math.min(range.maxY, Math.max(range.minY, y)) } +// 获取当前入口位置,未初始化时回退到默认位置。 function getCurrentFabPosition() { return fabPosition.value || getDefaultFabPosition() } +// 计算入口右侧与视口右边缘之间的距离。 function getFabRightEdgeOffset(position = getCurrentFabPosition()) { const viewport = getViewportSize() const size = getOpenFabSize() @@ -345,6 +364,7 @@ function getFabRightEdgeOffset(position = getCurrentFabPosition()) { return viewport.width - (position.x + size.width) } +// 计算入口纵向位置在可移动范围内的比例。 function getFabYRatio(position: FabPosition, options: FabPositionRangeOptions = {}) { return getFabFreePositionRatio(position, options).y } @@ -370,10 +390,12 @@ function updateFabAnchorFromPosition(position = getCurrentFabPosition(), options } } +// 判断当前锚点是否满足自动贴边收起条件。 function shouldFabAutoDock() { return fabPositionAnchor?.mode === 'right' && fabPositionAnchor.rightOffset <= FAB_RIGHT_EDGE_DOCK_DISTANCE } +// 把自由拖拽坐标转换成可随视口缩放恢复的比例坐标。 function getFabFreePositionRatio(position: FabPosition, options: FabPositionRangeOptions = {}): FabPositionRatio { const range = getFabPositionRange(options) const xRange = range.maxX - range.minX @@ -385,6 +407,7 @@ function getFabFreePositionRatio(position: FabPosition, options: FabPositionRang } } +// 根据比例坐标还原入口位置。 function getFabPositionFromRatio(ratio: FabPositionRatio, options: FabPositionRangeOptions = {}) { const range = getFabPositionRange(options) @@ -397,12 +420,14 @@ function getFabPositionFromRatio(ratio: FabPositionRatio, options: FabPositionRa ) } +// 根据纵向比例还原入口 Y 坐标。 function getFabYFromRatio(yRatio: number, options: FabPositionRangeOptions = {}) { const range = getFabPositionRange(options) return range.minY + (range.maxY - range.minY) * yRatio } +// 根据右侧贴边或自由位置锚点还原入口坐标。 function getFabPositionFromAnchor(anchor: FabPositionAnchor) { if (anchor.mode === 'right') { const viewport = getViewportSize() @@ -426,6 +451,7 @@ function getFabPositionFromAnchor(anchor: FabPositionAnchor) { ) } +// 获取从贴边状态开始拖拽时应恢复的展开位置。 function getOpenFabPositionForDrag(currentPosition: FabPosition) { if (fabPositionAnchor) return getFabPositionFromAnchor(fabPositionAnchor) @@ -441,6 +467,7 @@ function getOpenFabPositionForDrag(currentPosition: FabPosition) { ) } +// 更新入口位置并按需同步锚点和气泡位置。 function updateFabPosition(position: FabPosition, options: FabPositionRangeOptions & { syncAnchor?: boolean } = {}) { const rangeOptions = { useOpenBounds: options.useOpenBounds ?? fabDragging.value } @@ -454,10 +481,12 @@ function clampNumber(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)) } +// 获取入口根节点,优先使用组件 ref,兜底查询 DOM。 function getFabRootElement() { return fabRootRef.value || (document.querySelector('.agent-assistant-fab') as HTMLElement | null) } +// 获取气泡定位使用的机器人锚点区域。 function getFabAnchorRect() { const root = getFabRootElement() const bot = root?.querySelector('.agent-assistant-fab__bot') as HTMLElement | null @@ -474,6 +503,7 @@ function getFabAnchorRect() { return null } +// 获取气泡容器尺寸,未渲染完成时使用响应式兜底宽高。 function getFabBubbleSize() { const viewport = getViewportSize() const rect = fabBubbleRef.value?.getBoundingClientRect() @@ -485,6 +515,7 @@ function getFabBubbleSize() { } } +// 把气泡单轴坐标限制在视口安全边距内。 function clampBubbleAxis(value: number, size: number, viewportSize: number) { const min = FAB_BUBBLE_SAFE_MARGIN const max = Math.max(min, viewportSize - size - FAB_BUBBLE_SAFE_MARGIN) @@ -492,12 +523,14 @@ function clampBubbleAxis(value: number, size: number, viewportSize: number) { return clampNumber(value, min, max) } +// 把气泡箭头位置限制在气泡边缘安全范围内。 function clampBubbleArrow(value: number, size: number) { const margin = Math.min(FAB_BUBBLE_ARROW_MARGIN, size / 2) return clampNumber(value, margin, Math.max(margin, size - margin)) } +// 计算某个气泡候选位置的空间不足和偏移惩罚。 function getBubbleCandidatePenalty( candidate: FabBubbleCandidate, bubbleSize: { height: number; width: number }, @@ -528,6 +561,7 @@ function getBubbleCandidatePenalty( } } +// 根据机器人位置、气泡尺寸和视口空间选择最佳气泡布局。 function calculateFabBubbleLayout(): FabBubbleLayout | null { const rootRect = getFabRootElement()?.getBoundingClientRect() const anchorRect = getFabAnchorRect() @@ -594,6 +628,7 @@ function calculateFabBubbleLayout(): FabBubbleLayout | null { } } +// 在无法匹配具体气泡元素时推断箭头颜色来源。 function getFallbackFabBubbleArrowSource(placement: FabBubblePlacement): FabBubbleArrowSource { const fallbackBubble = placement === 'bottom' || placement === 'right' @@ -606,6 +641,7 @@ function getFallbackFabBubbleArrowSource(placement: FabBubblePlacement): FabBubb } } +// 计算气泡元素到箭头落点的距离,用于匹配箭头来源。 function getBubbleDistanceToArrow(rect: DOMRect, arrowX: number, arrowY: number, placement: FabBubblePlacement) { if (placement === 'left' || placement === 'right') { if (arrowY >= rect.top && arrowY <= rect.bottom) return 0 @@ -618,6 +654,7 @@ function getBubbleDistanceToArrow(rect: DOMRect, arrowX: number, arrowY: number, return Math.min(Math.abs(arrowX - rect.left), Math.abs(arrowX - rect.right)) } +// 同步当前箭头指向的气泡类型和状态颜色。 function syncFabBubbleArrowSource(layout: FabBubbleLayout, rootRect: DOMRect) { const bubbleElements = Array.from( fabBubbleRef.value?.querySelectorAll('.agent-assistant-fab__bubble') || [], @@ -651,6 +688,7 @@ function syncFabBubbleArrowSource(layout: FabBubbleLayout, rootRect: DOMRect) { : getFallbackFabBubbleArrowSource(layout.placement) } +// 把计算出的气泡位置和箭头位置写入 CSS 变量。 function syncFabBubblePosition() { if (!hasFabBubbles.value || !props.active) return @@ -669,6 +707,7 @@ function syncFabBubblePosition() { fabBubblePositioned.value = true } +// 使用 requestAnimationFrame 合并气泡位置更新。 function scheduleFabBubblePositionUpdate() { if (fabBubblePositionFrame || !hasFabBubbles.value) return @@ -678,6 +717,7 @@ function scheduleFabBubblePositionUpdate() { }) } +// 清理贴边展开后的延迟气泡定位计时器。 function clearFabBubbleUndockPositionTimer() { if (fabBubbleUndockPositionTimer === null) return @@ -685,6 +725,7 @@ function clearFabBubbleUndockPositionTimer() { fabBubbleUndockPositionTimer = null } +// 安排贴边展开动画后的气泡位置复算。 function scheduleFabBubblePostUndockPositionUpdate() { clearFabBubbleUndockPositionTimer() fabBubbleUndockPositionTimer = window.setTimeout(() => { @@ -693,6 +734,7 @@ function scheduleFabBubblePostUndockPositionUpdate() { }, FAB_BUBBLE_UNDOCK_POSITION_SYNC_DELAY) } +// 同步气泡 ResizeObserver,让内容高度变化后重新定位。 function syncFabBubbleResizeObserver() { fabBubbleResizeObserver?.disconnect() fabBubbleResizeObserver = null @@ -705,6 +747,7 @@ function syncFabBubbleResizeObserver() { fabBubbleResizeObserver.observe(fabBubbleRef.value) } +// 清理气泡定位相关的动画帧、观察器和计时器。 function teardownFabBubblePositioning() { if (fabBubblePositionFrame) { window.cancelAnimationFrame(fabBubblePositionFrame) @@ -716,6 +759,7 @@ function teardownFabBubblePositioning() { clearFabBubbleUndockPositionTimer() } +// 重置入口到默认位置并同步锚点和气泡位置。 function resetFabPosition() { fabPosition.value = getDefaultFabPosition() updateFabAnchorFromPosition(fabPosition.value) @@ -723,6 +767,7 @@ function resetFabPosition() { if (shouldFabAutoDock()) scheduleFabAutoDock() } +// 在窗口或可视视口变化时按锚点恢复入口位置。 function handleWindowResize() { const currentPosition = getCurrentFabPosition() if (fabDocked.value) { @@ -740,6 +785,7 @@ function handleWindowResize() { scheduleFabBubblePositionUpdate() } +// 把 Markdown 内容压缩成适合气泡预览的纯文本。 function stripMarkdownPreview(value: string) { return value .replace(/```[\s\S]*?```/g, ' ') @@ -804,6 +850,7 @@ function updateFabPointer(event: PointerEvent) { queueFabPointerUpdate(event.clientX, event.clientY) } +// 重置机器人按压状态和眼神跟随位移。 function resetFabPointer() { fabPressed.value = false fabPointerStyle.value = { @@ -819,6 +866,7 @@ function resetFabPointer() { } } +// 清理入口自动贴边计时器。 function clearFabIdleTimer() { if (fabIdleTimer === null) return @@ -826,6 +874,7 @@ function clearFabIdleTimer() { fabIdleTimer = null } +// 清理拖拽后抑制点击的恢复计时器。 function clearFabSuppressNextClickTimer() { if (fabSuppressNextClickTimer === null) return @@ -833,6 +882,7 @@ function clearFabSuppressNextClickTimer() { fabSuppressNextClickTimer = null } +// 拖拽结束后短暂抑制下一次点击,避免误打开面板。 function suppressNextFabClick() { fabSuppressNextClick = true clearFabSuppressNextClickTimer() @@ -842,6 +892,7 @@ function suppressNextFabClick() { }, FAB_DRAG_SUPPRESS_CLICK_DELAY) } +// 在入口靠近右侧边缘且空闲时安排自动贴边收起。 function scheduleFabAutoDock() { clearFabIdleTimer() if (fabDocked.value || hasKeepOpenFabBubbles.value || fabRandomAction.value || !shouldFabAutoDock()) return @@ -859,102 +910,11 @@ function scheduleFabAutoDock() { }, FAB_IDLE_DOCK_DELAY) } +// 暂停入口自动贴边收起。 function pauseFabAutoDock() { clearFabIdleTimer() } -// 返回下一次趣味动作的随机等待时间,让动作出现节奏更自然。 -function getFabRandomActionDelay() { - return ( - FAB_RANDOM_ACTION_MIN_DELAY + - Math.round(Math.random() * (FAB_RANDOM_ACTION_MAX_DELAY - FAB_RANDOM_ACTION_MIN_DELAY)) - ) -} - -// 判断当前交互状态是否适合播放随机动作,避免干扰半隐藏、拖拽和思考态。 -function canRunFabRandomAction() { - return props.active && !fabDocked.value && !fabDragging.value && !fabPressed.value && !props.thinking -} - -// 随机选择一个不同于上一次的趣味动作,减少连续重复带来的机械感。 -function pickFabRandomAction(): FabRandomAction { - const candidates = FAB_RANDOM_ACTIONS.filter(action => action !== fabLastRandomAction) - const action = candidates[Math.floor(Math.random() * candidates.length)] || FAB_RANDOM_ACTIONS[0] - - fabLastRandomAction = action - return action -} - -// 清理等待中的随机动作计时器。 -function clearFabRandomActionTimer() { - if (fabRandomActionTimer === null) return - - window.clearTimeout(fabRandomActionTimer) - fabRandomActionTimer = null -} - -// 清理正在播放动作的结束计时器。 -function clearFabRandomActionEndTimer() { - if (fabRandomActionEndTimer === null) return - - window.clearTimeout(fabRandomActionEndTimer) - fabRandomActionEndTimer = null -} - -// 停止当前随机动作并清理相关计时器。 -function clearFabRandomAction() { - clearFabRandomActionTimer() - clearFabRandomActionEndTimer() - fabRandomAction.value = null -} - -// 安排下一次随机动作,只在机器人完全可见且空闲时生效。 -function scheduleFabRandomAction() { - clearFabRandomActionTimer() - if (!canRunFabRandomAction() || fabRandomAction.value || fabRandomActionEndTimer !== null) return - - fabRandomActionTimer = window.setTimeout(() => { - fabRandomActionTimer = null - runFabRandomAction() - }, getFabRandomActionDelay()) -} - -// 完成当前随机动作后恢复空闲态,并继续排队下一次动作。 -function finishFabRandomAction() { - clearFabRandomActionEndTimer() - fabRandomAction.value = null - - const shouldAutoDock = !fabDocked.value && shouldFabAutoDock() - - if (shouldAutoDock) { - scheduleFabAutoDock() - return - } - - scheduleFabRandomAction() -} - -// 播放一个随机趣味动作,动作期间由 CSS 类驱动部件动画。 -function runFabRandomAction() { - if (!canRunFabRandomAction()) return - - const action = pickFabRandomAction() - - fabRandomAction.value = action - fabRandomActionEndTimer = window.setTimeout(finishFabRandomAction, FAB_RANDOM_ACTION_DURATIONS[action]) -} - -// 根据当前显示和交互状态同步随机动作队列。 -function syncFabRandomActionSchedule() { - if (canRunFabRandomAction()) { - if (!fabRandomAction.value && fabRandomActionTimer === null && fabRandomActionEndTimer === null) - scheduleFabRandomAction() - return - } - - clearFabRandomAction() -} - // 取消挂起的全局指针帧并移除监听器。 function teardownFabPointerTracking() { if (fabPointerFrame) { @@ -967,18 +927,22 @@ function teardownFabPointerTracking() { window.removeEventListener('pointerdown', handleGlobalFabPointer) } +// 构建通知气泡标题,缺省时回退到来源或通知中心文案。 function buildNotificationBubbleTitle(payload: AgentAssistantNotificationBubblePayload) { return payload.title || payload.source || payload.mtype || t('notification.center') } +// 构建通知气泡正文,并清理 Markdown 标记。 function buildNotificationBubbleText(payload: AgentAssistantNotificationBubblePayload) { return stripMarkdownPreview(payload.text || payload.title || payload.source || payload.mtype || '') } +// 获取气泡视觉状态,缺省使用默认状态。 function getBubbleVariant(payload: AgentAssistantBubblePayload): AgentAssistantBubbleVariant { return payload.variant || 'default' } +// 根据气泡状态选择标题图标。 function getBubbleIcon(variant: AgentAssistantBubbleVariant) { const icons: Record = { default: 'mdi-bell-outline', @@ -991,6 +955,7 @@ function getBubbleIcon(variant: AgentAssistantBubbleVariant) { return icons[variant] } +// 根据 toast 类型构建 Agent 气泡标题。 function getToastBubbleTitle(payload: AgentAssistantBubblePayload) { if (payload.title) return payload.title @@ -1005,6 +970,7 @@ function getToastBubbleTitle(payload: AgentAssistantBubblePayload) { return titles[getBubbleVariant(payload)] } +// 清理指定气泡的自动关闭计时器。 function clearFabBubbleTimer(id: string) { const timer = fabBubbleTimers.get(id) if (!timer) return @@ -1013,6 +979,7 @@ function clearFabBubbleTimer(id: string) { fabBubbleTimers.delete(id) } +// 安排气泡在指定时长后自动移除。 function scheduleFabBubbleRemoval(id: string, duration = FAB_NOTIFICATION_BUBBLE_DURATION) { clearFabBubbleTimer(id) fabBubbleTimers.set( @@ -1023,6 +990,7 @@ function scheduleFabBubbleRemoval(id: string, duration = FAB_NOTIFICATION_BUBBLE ) } +// 新增或更新气泡,并维护堆叠上限和定位。 function upsertFabBubble(bubble: AgentAssistantEntryBubble, options: { autoClose?: boolean; duration?: number } = {}) { if (!props.active || !bubble.text) return @@ -1047,6 +1015,7 @@ function upsertFabBubble(bubble: AgentAssistantEntryBubble, options: { autoClose if (options.autoClose) scheduleFabBubbleRemoval(bubble.id, options.duration) } +// 规范化气泡输入并展示到入口气泡堆叠。 function showBubble(input: AgentAssistantEntryBubbleInput) { const text = stripMarkdownPreview(input.text) if (!text) return @@ -1067,6 +1036,7 @@ function showBubble(input: AgentAssistantEntryBubbleInput) { ) } +// 展示助手回复预览气泡。 function showAssistantReplyPreview(value: string) { showBubble({ id: 'assistant-preview', @@ -1075,6 +1045,7 @@ function showAssistantReplyPreview(value: string) { }) } +// 展示通知中心推送过来的气泡。 function showNotificationBubble(payload: AgentAssistantNotificationBubblePayload) { const text = buildNotificationBubbleText(payload) if (!text) return @@ -1090,6 +1061,7 @@ function showNotificationBubble(payload: AgentAssistantNotificationBubblePayload }) } +// 展示全局 toast 路由过来的气泡。 function showToastBubble(payload: AgentAssistantBubblePayload) { const text = stripMarkdownPreview(payload.text || payload.title || '') if (!text) return @@ -1106,6 +1078,7 @@ function showToastBubble(payload: AgentAssistantBubblePayload) { }) } +// 根据全局气泡事件类型分发到通知或 toast 展示逻辑。 function showAgentAssistantBubble(payload: AgentAssistantBubblePayload) { if ((payload.kind || 'notification') === 'toast') { showToastBubble(payload) @@ -1115,6 +1088,12 @@ function showAgentAssistantBubble(payload: AgentAssistantBubblePayload) { showNotificationBubble(payload as AgentAssistantNotificationBubblePayload) } +// 主动播放一个宠物动作,供外部入口或后续复杂动画事件复用。 +function playPetAction(action: AgentPetActionName) { + playAgentPetAction(action) +} + +// 关闭指定气泡,未传 ID 时关闭全部气泡。 function closeBubble(id?: string) { if (id) { clearFabBubbleTimer(id) @@ -1133,10 +1112,12 @@ function closeBubble(id?: string) { }) } +// 清空入口上的全部气泡。 function clearBubbles() { closeBubble() } +// 清理所有气泡和自动关闭计时器。 function resetFabBubbles() { fabBubbles.value.forEach(item => clearFabBubbleTimer(item.id)) fabBubbles.value = [] @@ -1144,6 +1125,7 @@ function resetFabBubbles() { nextTick(syncFabBubbleResizeObserver) } +// 切换入口贴边收起状态并恢复对应位置。 function setFabDocked(docked: boolean) { const currentPosition = getCurrentFabPosition() @@ -1184,6 +1166,7 @@ function setFabDocked(docked: boolean) { }) } +// 清理拖拽状态和触摸移动拦截。 function clearFabDragState() { fabDragState = null fabDragging.value = false @@ -1191,6 +1174,7 @@ function clearFabDragState() { teardownFabTouchMoveGuard() } +// 释放指针捕获,容忍浏览器提前中断捕获的情况。 function releaseFabPointerCapture(event: PointerEvent) { try { ;(event.currentTarget as HTMLElement).releasePointerCapture?.(event.pointerId) @@ -1199,6 +1183,7 @@ function releaseFabPointerCapture(event: PointerEvent) { } } +// 取消当前拖拽并根据位置恢复自动贴边策略。 function cancelFabDrag() { const wasDragging = fabDragging.value @@ -1215,6 +1200,7 @@ function cancelFabDrag() { } } +// 拦截入口指针事件,按需阻止触摸默认滚动。 function guardFabPointerEvent(event: PointerEvent, options: { preventTouchDefault?: boolean } = {}) { event.stopPropagation() if ( @@ -1226,6 +1212,7 @@ function guardFabPointerEvent(event: PointerEvent, options: { preventTouchDefaul } } +// 在触摸拖拽期间拦截页面滚动。 function setupFabTouchMoveGuard() { if (stopFabTouchMoveGuard) return @@ -1243,10 +1230,12 @@ function setupFabTouchMoveGuard() { } } +// 移除触摸拖拽滚动拦截。 function teardownFabTouchMoveGuard() { stopFabTouchMoveGuard?.() } +// 判断当前指针事件是否仍处于按压拖拽状态。 function isPressedDragPointer(event: PointerEvent) { if (event.pointerType === 'touch' || event.pointerType === 'pen') { return event.buttons !== 0 || event.pressure > 0 @@ -1255,6 +1244,7 @@ function isPressedDragPointer(event: PointerEvent) { return event.buttons !== 0 } +// 处理入口触发区按下事件并初始化拖拽状态。 function handleFabTriggerPointerDown(event: PointerEvent) { guardFabPointerEvent(event) if (fabSuppressNextClick) { @@ -1282,6 +1272,7 @@ function handleFabTriggerPointerDown(event: PointerEvent) { } } +// 处理入口拖拽移动并同步位置和眼神跟随。 function handleFabTriggerPointerMove(event: PointerEvent) { guardFabPointerEvent(event, { preventTouchDefault: true }) updateFabPointer(event) @@ -1317,6 +1308,7 @@ function handleFabTriggerPointerMove(event: PointerEvent) { }) } +// 处理入口拖拽释放并决定是否贴边收起。 function handleFabTriggerPointerUp(event: PointerEvent) { guardFabPointerEvent(event) fabPressed.value = false @@ -1349,6 +1341,7 @@ function handleFabTriggerPointerUp(event: PointerEvent) { } } +// 处理指针取消事件并终止拖拽。 function handleFabTriggerPointerCancel(event: PointerEvent) { guardFabPointerEvent(event) fabPressed.value = false @@ -1360,18 +1353,21 @@ function handleFabTriggerPointerCancel(event: PointerEvent) { cancelFabDrag() } +// 处理指针捕获丢失时的拖拽收敛。 function handleFabTriggerLostPointerCapture(event: PointerEvent) { if (!fabDragState || fabDragState.pointerId !== event.pointerId) return cancelFabDrag() } +// 兜底处理窗口级指针结束事件。 function handleWindowFabPointerEnd(event: PointerEvent) { if (!fabDragState || fabDragState.pointerId !== event.pointerId) return cancelFabDrag() } +// 处理入口点击,贴边时先展开,否则打开助手面板。 function handleFabTriggerClick(event: MouseEvent) { event.stopPropagation() if (fabSuppressNextClick && event.detail !== 0) { @@ -1392,10 +1388,12 @@ function handleFabTriggerClick(event: MouseEvent) { emit('open') } +// 处理指针离开入口时的自动贴边排队。 function handleFabPointerLeave() { if (!fabDocked.value && shouldFabAutoDock()) scheduleFabAutoDock() } +// 处理指针进入入口时暂停自动贴边。 function handleFabPointerEnter() { pauseFabAutoDock() } @@ -1434,11 +1432,8 @@ watch( }, ) -watch([() => props.active, () => props.thinking, fabDocked, fabDragging, fabPressed], syncFabRandomActionSchedule) - onScopeDispose(clearFabIdleTimer) onScopeDispose(clearFabSuppressNextClickTimer) -onScopeDispose(clearFabRandomAction) onScopeDispose(resetFabBubbles) onScopeDispose(teardownFabBubblePositioning) onScopeDispose(clearFabBubbleUndockPositionTimer) @@ -1457,6 +1452,7 @@ onScopeDispose(() => { defineExpose({ clearBubbles, closeBubble, + playPetAction, setDocked: setFabDocked, showAssistantReplyPreview, showBubble, @@ -1531,23 +1527,7 @@ defineExpose({ @lostpointercapture="handleFabTriggerLostPointerCapture" @click="handleFabTriggerClick" > -