mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
fix: reposition agent assistant bubbles
This commit is contained in:
@@ -53,12 +53,17 @@ const FAB_TOAST_BUBBLE_DURATION = 4500
|
|||||||
const FAB_MAX_BUBBLES = 4
|
const FAB_MAX_BUBBLES = 4
|
||||||
const FAB_DEFAULT_RIGHT_OFFSET = 18
|
const FAB_DEFAULT_RIGHT_OFFSET = 18
|
||||||
const FAB_DEFAULT_VERTICAL_RATIO = 2 / 3
|
const FAB_DEFAULT_VERTICAL_RATIO = 2 / 3
|
||||||
|
const FAB_BUBBLE_GAP = 12
|
||||||
|
const FAB_BUBBLE_SAFE_MARGIN = 12
|
||||||
|
const FAB_BUBBLE_ARROW_MARGIN = 28
|
||||||
|
const FAB_BUBBLE_EDGE_ARROW_OFFSET = 38
|
||||||
const FAB_RANDOM_ACTION_MIN_DELAY = 8000
|
const FAB_RANDOM_ACTION_MIN_DELAY = 8000
|
||||||
const FAB_RANDOM_ACTION_MAX_DELAY = 18000
|
const FAB_RANDOM_ACTION_MAX_DELAY = 18000
|
||||||
|
|
||||||
const FAB_RANDOM_ACTIONS = ['wave', 'sit', 'eye-roll', 'faint', 'disassemble', 'happy-jump'] as const
|
const FAB_RANDOM_ACTIONS = ['wave', 'sit', 'eye-roll', 'faint', 'disassemble', 'happy-jump'] as const
|
||||||
|
|
||||||
type FabRandomAction = (typeof FAB_RANDOM_ACTIONS)[number]
|
type FabRandomAction = (typeof FAB_RANDOM_ACTIONS)[number]
|
||||||
|
type FabBubblePlacement = 'bottom' | 'left' | 'right' | 'top'
|
||||||
|
|
||||||
const FAB_RANDOM_ACTION_DURATIONS: Record<FabRandomAction, number> = {
|
const FAB_RANDOM_ACTION_DURATIONS: Record<FabRandomAction, number> = {
|
||||||
wave: 2450,
|
wave: 2450,
|
||||||
@@ -98,6 +103,23 @@ interface FabPointerPoint {
|
|||||||
clientY: number
|
clientY: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FabBubbleCandidate {
|
||||||
|
placement: FabBubblePlacement
|
||||||
|
idealX: number
|
||||||
|
idealY: number
|
||||||
|
weight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FabBubbleLayout {
|
||||||
|
arrowX: number
|
||||||
|
arrowY: number
|
||||||
|
placement: FabBubblePlacement
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const fabRootRef = ref<HTMLElement | null>(null)
|
||||||
|
const fabBubbleRef = ref<HTMLElement | null>(null)
|
||||||
const fabDocked = ref(false)
|
const fabDocked = ref(false)
|
||||||
const fabPosition = ref<FabPosition | null>(null)
|
const fabPosition = ref<FabPosition | null>(null)
|
||||||
const fabPointerStyle = ref({
|
const fabPointerStyle = ref({
|
||||||
@@ -120,6 +142,14 @@ const fabPositionStyle = computed(() => {
|
|||||||
'--agent-assistant-fab-y': `${position.y}px`,
|
'--agent-assistant-fab-y': `${position.y}px`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const fabBubblePlacement = ref<FabBubblePlacement>('top')
|
||||||
|
const fabBubblePositionStyle = ref({
|
||||||
|
'--agent-assistant-bubble-arrow-x': `${FAB_BUBBLE_EDGE_ARROW_OFFSET}px`,
|
||||||
|
'--agent-assistant-bubble-arrow-y': '50%',
|
||||||
|
'--agent-assistant-bubbles-x': '0px',
|
||||||
|
'--agent-assistant-bubbles-y': '0px',
|
||||||
|
})
|
||||||
|
const fabBubblePositioned = ref(false)
|
||||||
const fabPressed = ref(false)
|
const fabPressed = ref(false)
|
||||||
const fabBubbles = ref<AgentAssistantEntryBubble[]>([])
|
const fabBubbles = ref<AgentAssistantEntryBubble[]>([])
|
||||||
const fabDragging = ref(false)
|
const fabDragging = ref(false)
|
||||||
@@ -130,6 +160,8 @@ let fabDragState: FabDragState | null = null
|
|||||||
let fabSuppressNextClick = false
|
let fabSuppressNextClick = false
|
||||||
let fabPointerFrame = 0
|
let fabPointerFrame = 0
|
||||||
let fabPendingPointerPoint: FabPointerPoint | null = null
|
let fabPendingPointerPoint: FabPointerPoint | null = null
|
||||||
|
let fabBubblePositionFrame = 0
|
||||||
|
let fabBubbleResizeObserver: ResizeObserver | null = null
|
||||||
let fabLastRandomAction: FabRandomAction | null = null
|
let fabLastRandomAction: FabRandomAction | null = null
|
||||||
let fabRandomActionTimer: number | null = null
|
let fabRandomActionTimer: number | null = null
|
||||||
let fabRandomActionEndTimer: number | null = null
|
let fabRandomActionEndTimer: number | null = null
|
||||||
@@ -181,7 +213,7 @@ function getDockedFabX() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFabInteractiveBounds(): FabInteractiveBounds {
|
function getFabInteractiveBounds(): FabInteractiveBounds {
|
||||||
const root = document.querySelector('.agent-assistant-fab') as HTMLElement | null
|
const root = getFabRootElement()
|
||||||
const trigger = root?.querySelector('.agent-assistant-fab__trigger') as HTMLElement | null
|
const trigger = root?.querySelector('.agent-assistant-fab__trigger') as HTMLElement | null
|
||||||
const rootRect = root?.getBoundingClientRect()
|
const rootRect = root?.getBoundingClientRect()
|
||||||
const triggerRect = trigger?.getBoundingClientRect()
|
const triggerRect = trigger?.getBoundingClientRect()
|
||||||
@@ -202,7 +234,7 @@ function getFabInteractiveBounds(): FabInteractiveBounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFabSize() {
|
function getFabSize() {
|
||||||
const root = document.querySelector('.agent-assistant-fab') as HTMLElement | null
|
const root = getFabRootElement()
|
||||||
const rect = root?.getBoundingClientRect()
|
const rect = root?.getBoundingClientRect()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -253,6 +285,7 @@ function isFabNearRightEdge(position = getCurrentFabPosition()) {
|
|||||||
|
|
||||||
function updateFabPosition(position: FabPosition) {
|
function updateFabPosition(position: FabPosition) {
|
||||||
fabPosition.value = clampFabPosition(position)
|
fabPosition.value = clampFabPosition(position)
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将数值限制在指定范围内,避免指针和随机动作计算产生过大的位移。
|
// 将数值限制在指定范围内,避免指针和随机动作计算产生过大的位移。
|
||||||
@@ -260,8 +293,189 @@ function clampNumber(value: number, min: number, max: number) {
|
|||||||
return Math.min(max, Math.max(min, value))
|
return Math.min(max, Math.max(min, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const trigger = root?.querySelector('.agent-assistant-fab__trigger') as HTMLElement | null
|
||||||
|
const botRect = bot?.getBoundingClientRect()
|
||||||
|
const triggerRect = trigger?.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (botRect && botRect.width > 0 && botRect.height > 0) return botRect
|
||||||
|
if (triggerRect && triggerRect.width > 0 && triggerRect.height > 0) return triggerRect
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFabBubbleSize() {
|
||||||
|
const viewport = getViewportSize()
|
||||||
|
const rect = fabBubbleRef.value?.getBoundingClientRect()
|
||||||
|
const fallbackWidth = Math.min(viewport.width - FAB_BUBBLE_SAFE_MARGIN * 2, viewport.width <= 600 ? 264 : 304)
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: rect && rect.height > 0 ? rect.height : 160,
|
||||||
|
width: rect && rect.width > 0 ? rect.width : Math.max(0, fallbackWidth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 },
|
||||||
|
anchorRect: DOMRect,
|
||||||
|
viewport: { height: number; width: number },
|
||||||
|
) {
|
||||||
|
const x = clampBubbleAxis(candidate.idealX, bubbleSize.width, viewport.width)
|
||||||
|
const y = clampBubbleAxis(candidate.idealY, bubbleSize.height, viewport.height)
|
||||||
|
const primaryAvailable = {
|
||||||
|
bottom: viewport.height - anchorRect.bottom - FAB_BUBBLE_SAFE_MARGIN,
|
||||||
|
left: anchorRect.left - FAB_BUBBLE_SAFE_MARGIN,
|
||||||
|
right: viewport.width - anchorRect.right - FAB_BUBBLE_SAFE_MARGIN,
|
||||||
|
top: anchorRect.top - FAB_BUBBLE_SAFE_MARGIN,
|
||||||
|
}[candidate.placement]
|
||||||
|
const primaryRequired =
|
||||||
|
candidate.placement === 'left' || candidate.placement === 'right'
|
||||||
|
? bubbleSize.width + FAB_BUBBLE_GAP
|
||||||
|
: bubbleSize.height + FAB_BUBBLE_GAP
|
||||||
|
const fitPenalty = Math.max(0, primaryRequired - primaryAvailable) * 8
|
||||||
|
const alignmentPenalty = Math.abs(x - candidate.idealX) + Math.abs(y - candidate.idealY)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...candidate,
|
||||||
|
score: fitPenalty + alignmentPenalty + candidate.weight,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateFabBubbleLayout(): FabBubbleLayout | null {
|
||||||
|
const rootRect = getFabRootElement()?.getBoundingClientRect()
|
||||||
|
const anchorRect = getFabAnchorRect()
|
||||||
|
if (!rootRect || !anchorRect) return null
|
||||||
|
|
||||||
|
const viewport = getViewportSize()
|
||||||
|
const bubbleSize = getFabBubbleSize()
|
||||||
|
const anchorCenterX = anchorRect.left + anchorRect.width / 2
|
||||||
|
const anchorCenterY = anchorRect.top + anchorRect.height / 2
|
||||||
|
const candidates: FabBubbleCandidate[] = [
|
||||||
|
{
|
||||||
|
idealX: anchorCenterX - bubbleSize.width / 2,
|
||||||
|
idealY: anchorRect.top - bubbleSize.height - FAB_BUBBLE_GAP,
|
||||||
|
placement: 'top',
|
||||||
|
weight: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idealX: anchorCenterX - bubbleSize.width / 2,
|
||||||
|
idealY: anchorRect.bottom + FAB_BUBBLE_GAP,
|
||||||
|
placement: 'bottom',
|
||||||
|
weight: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idealX: anchorRect.right + FAB_BUBBLE_GAP,
|
||||||
|
idealY: anchorCenterY - bubbleSize.height / 2,
|
||||||
|
placement: 'right',
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idealX: anchorRect.left - bubbleSize.width - FAB_BUBBLE_GAP,
|
||||||
|
idealY: anchorCenterY - bubbleSize.height / 2,
|
||||||
|
placement: 'left',
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const bestCandidate = candidates
|
||||||
|
.map(candidate => getBubbleCandidatePenalty(candidate, bubbleSize, anchorRect, viewport))
|
||||||
|
.sort((a, b) => a.score - b.score)[0]
|
||||||
|
|
||||||
|
if (!bestCandidate) return null
|
||||||
|
|
||||||
|
const arrowX =
|
||||||
|
bestCandidate.placement === 'left'
|
||||||
|
? bubbleSize.width
|
||||||
|
: bestCandidate.placement === 'right'
|
||||||
|
? 0
|
||||||
|
: clampBubbleArrow(anchorCenterX - bestCandidate.x, bubbleSize.width)
|
||||||
|
const arrowY =
|
||||||
|
bestCandidate.placement === 'top'
|
||||||
|
? bubbleSize.height
|
||||||
|
: bestCandidate.placement === 'bottom'
|
||||||
|
? 0
|
||||||
|
: clampBubbleArrow(anchorCenterY - bestCandidate.y, bubbleSize.height)
|
||||||
|
|
||||||
|
return {
|
||||||
|
arrowX,
|
||||||
|
arrowY,
|
||||||
|
placement: bestCandidate.placement,
|
||||||
|
x: bestCandidate.x - rootRect.left,
|
||||||
|
y: bestCandidate.y - rootRect.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFabBubblePosition() {
|
||||||
|
if (!hasFabBubbles.value || !props.active) return
|
||||||
|
|
||||||
|
const layout = calculateFabBubbleLayout()
|
||||||
|
if (!layout) return
|
||||||
|
|
||||||
|
fabBubblePlacement.value = layout.placement
|
||||||
|
fabBubblePositionStyle.value = {
|
||||||
|
'--agent-assistant-bubble-arrow-x': `${Math.round(layout.arrowX)}px`,
|
||||||
|
'--agent-assistant-bubble-arrow-y': `${Math.round(layout.arrowY)}px`,
|
||||||
|
'--agent-assistant-bubbles-x': `${Math.round(layout.x)}px`,
|
||||||
|
'--agent-assistant-bubbles-y': `${Math.round(layout.y)}px`,
|
||||||
|
}
|
||||||
|
fabBubblePositioned.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFabBubblePositionUpdate() {
|
||||||
|
if (fabBubblePositionFrame || !hasFabBubbles.value) return
|
||||||
|
|
||||||
|
fabBubblePositionFrame = window.requestAnimationFrame(() => {
|
||||||
|
fabBubblePositionFrame = 0
|
||||||
|
syncFabBubblePosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFabBubbleResizeObserver() {
|
||||||
|
fabBubbleResizeObserver?.disconnect()
|
||||||
|
fabBubbleResizeObserver = null
|
||||||
|
|
||||||
|
if (!fabBubbleRef.value || typeof ResizeObserver === 'undefined') return
|
||||||
|
|
||||||
|
fabBubbleResizeObserver = new ResizeObserver(() => {
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
|
})
|
||||||
|
fabBubbleResizeObserver.observe(fabBubbleRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardownFabBubblePositioning() {
|
||||||
|
if (fabBubblePositionFrame) {
|
||||||
|
window.cancelAnimationFrame(fabBubblePositionFrame)
|
||||||
|
fabBubblePositionFrame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fabBubbleResizeObserver?.disconnect()
|
||||||
|
fabBubbleResizeObserver = null
|
||||||
|
}
|
||||||
|
|
||||||
function resetFabPosition() {
|
function resetFabPosition() {
|
||||||
fabPosition.value = getDefaultFabPosition()
|
fabPosition.value = getDefaultFabPosition()
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +487,7 @@ function handleWindowResize() {
|
|||||||
x: getDockedFabX(),
|
x: getDockedFabX(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripMarkdownPreview(value: string) {
|
function stripMarkdownPreview(value: string) {
|
||||||
@@ -543,8 +758,14 @@ function scheduleFabBubbleRemoval(id: string, duration = FAB_NOTIFICATION_BUBBLE
|
|||||||
function upsertFabBubble(bubble: AgentAssistantEntryBubble, options: { autoClose?: boolean; duration?: number } = {}) {
|
function upsertFabBubble(bubble: AgentAssistantEntryBubble, options: { autoClose?: boolean; duration?: number } = {}) {
|
||||||
if (!props.active || !bubble.text) return
|
if (!props.active || !bubble.text) return
|
||||||
|
|
||||||
|
const hadBubbles = hasFabBubbles.value
|
||||||
const existingBubbles = fabBubbles.value.filter(item => item.id !== bubble.id)
|
const existingBubbles = fabBubbles.value.filter(item => item.id !== bubble.id)
|
||||||
|
if (!hadBubbles) fabBubblePositioned.value = false
|
||||||
fabBubbles.value = [bubble, ...existingBubbles].slice(0, FAB_MAX_BUBBLES)
|
fabBubbles.value = [bubble, ...existingBubbles].slice(0, FAB_MAX_BUBBLES)
|
||||||
|
nextTick(() => {
|
||||||
|
syncFabBubbleResizeObserver()
|
||||||
|
syncFabBubblePosition()
|
||||||
|
})
|
||||||
|
|
||||||
// 超出堆叠上限的气泡需要同步清理计时器,避免后续 timer 访问过期项。
|
// 超出堆叠上限的气泡需要同步清理计时器,避免后续 timer 访问过期项。
|
||||||
const visibleIds = new Set(fabBubbles.value.map(item => item.id))
|
const visibleIds = new Set(fabBubbles.value.map(item => item.id))
|
||||||
@@ -633,7 +854,13 @@ function closeBubble(id?: string) {
|
|||||||
fabBubbles.value = []
|
fabBubbles.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fabBubbles.value.length) fabBubblePositioned.value = false
|
||||||
|
|
||||||
if (!hasKeepOpenFabBubbles.value) scheduleFabAutoDock()
|
if (!hasKeepOpenFabBubbles.value) scheduleFabAutoDock()
|
||||||
|
nextTick(() => {
|
||||||
|
syncFabBubbleResizeObserver()
|
||||||
|
syncFabBubblePosition()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearBubbles() {
|
function clearBubbles() {
|
||||||
@@ -643,6 +870,8 @@ function clearBubbles() {
|
|||||||
function resetFabBubbles() {
|
function resetFabBubbles() {
|
||||||
fabBubbles.value.forEach(item => clearFabBubbleTimer(item.id))
|
fabBubbles.value.forEach(item => clearFabBubbleTimer(item.id))
|
||||||
fabBubbles.value = []
|
fabBubbles.value = []
|
||||||
|
fabBubblePositioned.value = false
|
||||||
|
nextTick(syncFabBubbleResizeObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFabDocked(docked: boolean) {
|
function setFabDocked(docked: boolean) {
|
||||||
@@ -657,6 +886,7 @@ function setFabDocked(docked: boolean) {
|
|||||||
...currentPosition,
|
...currentPosition,
|
||||||
x: getDockedFabX(),
|
x: getDockedFabX(),
|
||||||
}
|
}
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,6 +897,7 @@ function setFabDocked(docked: boolean) {
|
|||||||
Math.max(0, getViewportSize().width - getOpenFabSize().width - FAB_DEFAULT_RIGHT_OFFSET),
|
Math.max(0, getViewportSize().width - getOpenFabSize().width - FAB_DEFAULT_RIGHT_OFFSET),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
scheduleFabAutoDock()
|
scheduleFabAutoDock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,6 +998,10 @@ watch(
|
|||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
||||||
|
nextTick(() => {
|
||||||
|
syncFabBubbleResizeObserver()
|
||||||
|
syncFabBubblePosition()
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,6 +1017,7 @@ watch([() => props.active, () => props.thinking, fabDocked, fabDragging, fabPres
|
|||||||
onScopeDispose(clearFabIdleTimer)
|
onScopeDispose(clearFabIdleTimer)
|
||||||
onScopeDispose(clearFabRandomAction)
|
onScopeDispose(clearFabRandomAction)
|
||||||
onScopeDispose(resetFabBubbles)
|
onScopeDispose(resetFabBubbles)
|
||||||
|
onScopeDispose(teardownFabBubblePositioning)
|
||||||
onScopeDispose(() => {
|
onScopeDispose(() => {
|
||||||
setAgentAssistantBubbleEntryActive(false)
|
setAgentAssistantBubbleEntryActive(false)
|
||||||
stopBubbleListener?.()
|
stopBubbleListener?.()
|
||||||
@@ -804,6 +1040,7 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-show="props.active"
|
v-show="props.active"
|
||||||
|
ref="fabRootRef"
|
||||||
class="agent-assistant-fab"
|
class="agent-assistant-fab"
|
||||||
:class="{
|
:class="{
|
||||||
'is-docked': fabDocked,
|
'is-docked': fabDocked,
|
||||||
@@ -811,6 +1048,7 @@ defineExpose({
|
|||||||
'is-pressed': fabPressed,
|
'is-pressed': fabPressed,
|
||||||
'is-thinking': props.thinking,
|
'is-thinking': props.thinking,
|
||||||
'is-bubble-visible': hasFabBubbles,
|
'is-bubble-visible': hasFabBubbles,
|
||||||
|
'is-bubble-positioned': fabBubblePositioned,
|
||||||
[`is-action-${fabRandomAction}`]: fabRandomAction,
|
[`is-action-${fabRandomAction}`]: fabRandomAction,
|
||||||
}"
|
}"
|
||||||
:style="fabPositionStyle"
|
:style="fabPositionStyle"
|
||||||
@@ -818,28 +1056,37 @@ defineExpose({
|
|||||||
@pointerenter="handleFabPointerEnter"
|
@pointerenter="handleFabPointerEnter"
|
||||||
@pointerleave="handleFabPointerLeave"
|
@pointerleave="handleFabPointerLeave"
|
||||||
>
|
>
|
||||||
<div v-if="hasFabBubbles" class="agent-assistant-fab__bubbles" aria-live="polite">
|
<div
|
||||||
<div
|
v-if="hasFabBubbles"
|
||||||
v-for="bubble in fabBubbles"
|
ref="fabBubbleRef"
|
||||||
:key="bubble.id"
|
class="agent-assistant-fab__bubbles"
|
||||||
class="agent-assistant-fab__bubble"
|
:class="`agent-assistant-fab__bubbles--${fabBubblePlacement}`"
|
||||||
:class="[`agent-assistant-fab__bubble--${bubble.kind}`, `agent-assistant-fab__bubble--${bubble.variant}`]"
|
:style="fabBubblePositionStyle"
|
||||||
role="status"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<strong v-if="bubble.title" class="agent-assistant-fab__bubble-title">
|
<div class="agent-assistant-fab__bubble-stack">
|
||||||
<VIcon class="agent-assistant-fab__bubble-icon" :icon="getBubbleIcon(bubble.variant)" size="20" />
|
<div
|
||||||
<span>{{ bubble.title }}</span>
|
v-for="bubble in fabBubbles"
|
||||||
</strong>
|
:key="bubble.id"
|
||||||
<span>{{ bubble.text }}</span>
|
class="agent-assistant-fab__bubble"
|
||||||
<button
|
:class="[`agent-assistant-fab__bubble--${bubble.kind}`, `agent-assistant-fab__bubble--${bubble.variant}`]"
|
||||||
class="agent-assistant-fab__bubble-close"
|
role="status"
|
||||||
type="button"
|
|
||||||
:aria-label="t('common.close')"
|
|
||||||
:title="t('common.close')"
|
|
||||||
@click.stop="closeBubble(bubble.id)"
|
|
||||||
>
|
>
|
||||||
<VIcon icon="mdi-close" size="14" />
|
<strong v-if="bubble.title" class="agent-assistant-fab__bubble-title">
|
||||||
</button>
|
<VIcon class="agent-assistant-fab__bubble-icon" :icon="getBubbleIcon(bubble.variant)" size="20" />
|
||||||
|
<span>{{ bubble.title }}</span>
|
||||||
|
</strong>
|
||||||
|
<span>{{ bubble.text }}</span>
|
||||||
|
<button
|
||||||
|
class="agent-assistant-fab__bubble-close"
|
||||||
|
type="button"
|
||||||
|
:aria-label="t('common.close')"
|
||||||
|
:title="t('common.close')"
|
||||||
|
@click.stop="closeBubble(bubble.id)"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-close" size="14" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -896,6 +1143,7 @@ defineExpose({
|
|||||||
--agent-assistant-robot-play: #fff;
|
--agent-assistant-robot-play: #fff;
|
||||||
--agent-assistant-robot-shadow: rgba(54, 0, 126, 28%);
|
--agent-assistant-robot-shadow: rgba(54, 0, 126, 28%);
|
||||||
--agent-assistant-robot-shadow-strong: rgba(54, 0, 126, 34%);
|
--agent-assistant-robot-shadow-strong: rgba(54, 0, 126, 34%);
|
||||||
|
--agent-assistant-bubble-bg: rgba(var(--v-theme-surface), 0.92);
|
||||||
--agent-assistant-bot-scale: 1;
|
--agent-assistant-bot-scale: 1;
|
||||||
--agent-assistant-bot-pressed-scale: 0.96;
|
--agent-assistant-bot-pressed-scale: 0.96;
|
||||||
--agent-assistant-fab-x: calc(100vw - 14.3rem);
|
--agent-assistant-fab-x: calc(100vw - 14.3rem);
|
||||||
@@ -962,23 +1210,35 @@ defineExpose({
|
|||||||
|
|
||||||
.agent-assistant-fab__bubbles {
|
.agent-assistant-fab__bubbles {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: grid;
|
display: block;
|
||||||
gap: 0.45rem;
|
|
||||||
inline-size: clamp(15.5rem, 22vw, 19rem);
|
inline-size: clamp(15.5rem, 22vw, 19rem);
|
||||||
inset-block-end: 4.45rem;
|
inset-block-start: 0;
|
||||||
inset-inline-end: 2.75rem;
|
inset-inline-start: 0;
|
||||||
max-block-size: min(34rem, calc(100vh - 8rem));
|
max-block-size: min(34rem, calc(100vh - 8rem));
|
||||||
max-inline-size: calc(100vw - 6.4rem);
|
max-inline-size: calc(100vw - 1.5rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
overflow-y: auto;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateY(0.22rem) scale(0.96);
|
transform: translate3d(var(--agent-assistant-bubbles-x), var(--agent-assistant-bubbles-y), 0) scale(0.96);
|
||||||
transform-origin: 100% 100%;
|
transform-origin: var(--agent-assistant-bubble-arrow-x) var(--agent-assistant-bubble-arrow-y);
|
||||||
transition:
|
transition:
|
||||||
opacity 0.2s ease,
|
opacity 0.2s ease,
|
||||||
transform 0.24s ease;
|
transform 0.24s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubble-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
max-block-size: inherit;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.12rem;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubble-stack::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bubble {
|
.agent-assistant-fab__bubble {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -989,7 +1249,7 @@ defineExpose({
|
|||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
background: rgba(var(--v-theme-surface), 0.92);
|
background: var(--agent-assistant-bubble-bg);
|
||||||
box-shadow: var(--app-surface-shadow);
|
box-shadow: var(--app-surface-shadow);
|
||||||
padding-block: 0.7rem;
|
padding-block: 0.7rem;
|
||||||
padding-inline: 0.85rem 1.85rem;
|
padding-inline: 0.85rem 1.85rem;
|
||||||
@@ -1060,20 +1320,49 @@ defineExpose({
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bubble::before {
|
.agent-assistant-fab__bubbles::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
background: inherit;
|
background: var(--agent-assistant-bubble-bg);
|
||||||
block-size: 0.62rem;
|
block-size: 0.62rem;
|
||||||
border-block-start: 0;
|
border-block-start: 0;
|
||||||
border-inline-start: 0;
|
border-inline-start: 0;
|
||||||
content: '';
|
content: '';
|
||||||
inline-size: 0.62rem;
|
inline-size: 0.62rem;
|
||||||
inset-block-end: -0.34rem;
|
inset-block-start: var(--agent-assistant-bubble-arrow-y);
|
||||||
inset-inline-end: 1.85rem;
|
inset-inline-start: var(--agent-assistant-bubble-arrow-x);
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubbles--top::before {
|
||||||
|
margin-block-start: -0.31rem;
|
||||||
|
margin-inline-start: -0.31rem;
|
||||||
|
border-block-start: 0;
|
||||||
|
border-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubbles--bottom::before {
|
||||||
|
margin-block-start: -0.31rem;
|
||||||
|
margin-inline-start: -0.31rem;
|
||||||
|
border-block-end: 0;
|
||||||
|
border-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubbles--left::before {
|
||||||
|
margin-block-start: -0.31rem;
|
||||||
|
margin-inline-start: -0.31rem;
|
||||||
|
border-block-end: 0;
|
||||||
|
border-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubbles--right::before {
|
||||||
|
margin-block-start: -0.31rem;
|
||||||
|
margin-inline-start: -0.31rem;
|
||||||
|
border-block-start: 0;
|
||||||
|
border-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bubble-close {
|
.agent-assistant-fab__bubble-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -1107,20 +1396,20 @@ defineExpose({
|
|||||||
color: rgba(var(--v-theme-on-surface), 0.86) !important;
|
color: rgba(var(--v-theme-on-surface), 0.86) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab.is-bubble-visible:not(.is-docked) .agent-assistant-fab__bubble {
|
.agent-assistant-fab.is-bubble-visible.is-bubble-positioned:not(.is-docked) .agent-assistant-fab__bubble {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab.is-bubble-visible:not(.is-docked) .agent-assistant-fab__bubbles {
|
.agent-assistant-fab.is-bubble-visible.is-bubble-positioned:not(.is-docked) .agent-assistant-fab__bubbles {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transform: translateY(0) scale(1);
|
transform: translate3d(var(--agent-assistant-bubbles-x), var(--agent-assistant-bubbles-y), 0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab.is-docked .agent-assistant-fab__bubbles {
|
.agent-assistant-fab.is-docked .agent-assistant-fab__bubbles {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateX(1.4rem) scale(0.9);
|
transform: translate3d(var(--agent-assistant-bubbles-x), var(--agent-assistant-bubbles-y), 0) scale(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bot,
|
.agent-assistant-fab__bot,
|
||||||
@@ -2453,12 +2742,14 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bubbles {
|
.agent-assistant-fab__bubbles {
|
||||||
gap: 0.38rem;
|
|
||||||
inline-size: min(16.5rem, calc(100vw - 5.6rem));
|
inline-size: min(16.5rem, calc(100vw - 5.6rem));
|
||||||
inset-inline-end: 2.35rem;
|
|
||||||
max-block-size: min(30rem, calc(100vh - 9.2rem));
|
max-block-size: min(30rem, calc(100vh - 9.2rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubble-stack {
|
||||||
|
gap: 0.38rem;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bubble {
|
.agent-assistant-fab__bubble {
|
||||||
padding-block: 0.56rem;
|
padding-block: 0.56rem;
|
||||||
padding-inline: 0.72rem 1.62rem;
|
padding-inline: 0.72rem 1.62rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user