refactor(ai-chat): enhance AISmartSidebar with spring animations and improved mouse interaction handling

- Replaced mouse position tracking with spring-based intensity for smoother animations.
- Introduced multiple transform properties for layered visual effects.
- Improved event handling for pointer movements and visibility of the prompt.
- Cleaned up code for better readability and performance.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-11-08 15:22:05 +08:00
parent 3832e21b7c
commit 0a42795377
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
1 changed files with 118 additions and 63 deletions

View File

@ -3,9 +3,8 @@ import "./AISmartSidebar.css"
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
import { Spring } from "@follow/components/constants/spring.js"
import { useMousePosition } from "@follow/components/hooks/useMouse.js"
import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
import { AnimatePresence, m } from "motion/react"
import { AnimatePresence, m, useSpring, useTransform } from "motion/react"
import * as React from "react"
import { useEffect, useState } from "react"
@ -15,43 +14,118 @@ import { COMMAND_ID } from "~/modules/command/commands/id"
import { useCommandShortcut } from "~/modules/command/hooks/use-command-binding"
const AIAmbientSidebar: React.FC<{ onExpand: () => void }> = ({ onExpand }) => {
const [intensity, setIntensity] = useState(0)
const [showPrompt, setShowPrompt] = useState(false)
const isShowPromptRef = React.useRef(false)
const mousePosition = useMousePosition()
const intensity = useSpring(0, Spring.presets.smooth)
const layer3Width = useTransform(intensity, (value) => (value > 0.1 ? 1 + value * 3 : 0))
const layer3Opacity = useTransform(intensity, (value) => value * 0.15)
const layer3X = useTransform(intensity, (value) => value * -8)
const layer2Width = useTransform(intensity, (value) => (value > 0.2 ? 1.5 + value * 4 : 0))
const layer2Opacity = useTransform(intensity, (value) => value * 0.25)
const layer2BoxShadow = useTransform(intensity, (value) =>
value > 0.3 ? `0 0 ${12 + value * 20}px rgba(255, 92, 0, ${value * 0.15})` : "none",
)
const layer2X = useTransform(intensity, (value) => value * -4)
const layer1Width = useTransform(intensity, (value) => (value > 0 ? 2 + value * 6 : 1))
const layer1Opacity = useTransform(intensity, (value) => value * 0.6)
const layer1BoxShadow = useTransform(intensity, (value) =>
value > 0.5 ? `0 0 ${16 + value * 24}px rgba(255, 92, 0, ${value * 0.25})` : "none",
)
const layer1Background = useTransform(intensity, (value) => {
const primaryAlpha = Math.min(1, Math.max(0, value * 0.4))
const secondaryAlpha = Math.min(1, Math.max(0, value * 0.2))
return `linear-gradient(to left, rgba(255, 92, 0, ${primaryAlpha}), rgba(255, 140, 0, ${secondaryAlpha}), transparent)`
})
const glowOpacity = useTransform(intensity, (value) => (value <= 0.4 ? 0 : (value - 0.4) * 0.3))
const glowBackground = useTransform(intensity, (value) => {
const alpha = value <= 0.4 ? 0 : (value - 0.4) * 0.12
return `radial-gradient(ellipse at center, rgba(255, 92, 0, ${alpha}) 0%, transparent 70%)`
})
const glowX = useTransform(intensity, (value) => value * -20)
const canShowPrompt = useGlobalFocusableScopeSelector(FocusablePresets.isNotFloatingLayerScope)
useEffect(() => {
if (!canShowPrompt) return
const rightEdgeDistance = window.innerWidth - mousePosition.x
if (!canShowPrompt) {
intensity.set(0)
if (isShowPromptRef.current) {
isShowPromptRef.current = false
setShowPrompt(false)
}
return
}
const maxDistance = 500
const threshold = 80
const showedThreshold = 300
const topBoundary = 100
const frameRef = { current: null as number | null }
if (mousePosition.y <= topBoundary) {
setIntensity(0)
setShowPrompt(false)
isShowPromptRef.current = false
return
const hidePrompt = () => {
intensity.set(0)
if (isShowPromptRef.current) {
isShowPromptRef.current = false
setShowPrompt(false)
}
}
if (isShowPromptRef.current && rightEdgeDistance <= showedThreshold) {
return
const handlePointerMove = (event: PointerEvent) => {
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
}
const { clientX, clientY } = event
frameRef.current = window.requestAnimationFrame(() => {
frameRef.current = null
const rightEdgeDistance = window.innerWidth - clientX
if (clientY <= topBoundary) {
hidePrompt()
return
}
if (rightEdgeDistance <= maxDistance) {
const newIntensity = Math.max(0, (maxDistance - rightEdgeDistance) / maxDistance)
intensity.set(newIntensity)
if (isShowPromptRef.current && rightEdgeDistance <= showedThreshold) {
return
}
const shouldShow = rightEdgeDistance <= threshold
if (shouldShow !== isShowPromptRef.current) {
isShowPromptRef.current = shouldShow
setShowPrompt(shouldShow)
}
} else {
hidePrompt()
}
})
}
if (rightEdgeDistance <= maxDistance) {
const newIntensity = Math.max(0, (maxDistance - rightEdgeDistance) / maxDistance)
setIntensity(newIntensity)
const shouldShow = rightEdgeDistance <= threshold
setShowPrompt(shouldShow)
isShowPromptRef.current = shouldShow
} else {
setIntensity(0)
setShowPrompt(false)
isShowPromptRef.current = false
const handlePointerLeave = () => {
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
hidePrompt()
}
}, [canShowPrompt, mousePosition])
window.addEventListener("pointermove", handlePointerMove, { passive: true })
window.addEventListener("pointerleave", handlePointerLeave)
return () => {
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
}
window.removeEventListener("pointermove", handlePointerMove)
window.removeEventListener("pointerleave", handlePointerLeave)
}
}, [canShowPrompt, intensity])
const toggleAIChatShortcut = useCommandShortcut(COMMAND_ID.global.toggleAIChat)
if (!canShowPrompt) return null
@ -64,10 +138,10 @@ const AIAmbientSidebar: React.FC<{ onExpand: () => void }> = ({ onExpand }) => {
<m.div
className="ai-glass-layer-3 absolute right-0 top-0 h-full"
style={{
width: intensity > 0.1 ? 1 + intensity * 3 : 0,
opacity: intensity * 0.15,
width: layer3Width,
opacity: layer3Opacity,
background: "linear-gradient(to left, rgba(255, 92, 0, 0.15), transparent)",
transform: `translateX(${intensity * -8}px)`,
x: layer3X,
}}
/>
@ -75,14 +149,11 @@ const AIAmbientSidebar: React.FC<{ onExpand: () => void }> = ({ onExpand }) => {
<m.div
className="ai-glass-layer-2 absolute right-0 top-0 h-full"
style={{
width: intensity > 0.2 ? 1.5 + intensity * 4 : 0,
opacity: intensity * 0.25,
width: layer2Width,
opacity: layer2Opacity,
background: "linear-gradient(to left, rgba(255, 92, 0, 0.2), transparent)",
transform: `translateX(${intensity * -4}px)`,
boxShadow:
intensity > 0.3
? `0 0 ${12 + intensity * 20}px rgba(255, 92, 0, ${intensity * 0.15})`
: "none",
x: layer2X,
boxShadow: layer2BoxShadow,
}}
/>
@ -90,36 +161,23 @@ const AIAmbientSidebar: React.FC<{ onExpand: () => void }> = ({ onExpand }) => {
<m.div
className="ai-glass-layer-1 absolute right-0 top-0 h-full"
style={{
width: intensity > 0 ? 2 + intensity * 6 : 1,
opacity: intensity * 0.6,
background: `linear-gradient(to left,
rgba(255, 92, 0, ${intensity * 0.4}),
rgba(255, 140, 0, ${intensity * 0.2}),
transparent)`,
boxShadow:
intensity > 0.5
? `0 0 ${16 + intensity * 24}px rgba(255, 92, 0, ${intensity * 0.25})`
: "none",
width: layer1Width,
opacity: layer1Opacity,
background: layer1Background,
boxShadow: layer1BoxShadow,
}}
/>
{/* Subtle ambient glow */}
{intensity > 0.4 && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute right-0 top-1/2 size-32 -translate-y-1/2"
style={{
opacity: (intensity - 0.4) * 0.3,
background: `radial-gradient(ellipse at center,
rgba(255, 92, 0, ${(intensity - 0.4) * 0.12}) 0%,
transparent 70%)`,
filter: "blur(30px)",
transform: `translateY(-50%) translateX(${intensity * -20}px)`,
}}
/>
)}
<m.div
className="absolute right-0 top-1/2 size-32 -translate-y-1/2"
style={{
opacity: glowOpacity,
background: glowBackground,
filter: "blur(30px)",
x: glowX,
}}
/>
</div>
<AnimatePresence>
@ -130,9 +188,6 @@ const AIAmbientSidebar: React.FC<{ onExpand: () => void }> = ({ onExpand }) => {
exit={{ opacity: 0, x: 30, scale: 0.95 }}
transition={Spring.presets.smooth}
className="fixed bottom-12 right-6 z-50 flex flex-col items-end gap-3"
style={{
transform: `translateX(${intensity * -3}px)`,
}}
>
{/* Unified glass card with integrated button */}
<m.div